From 47808d2d08d8b5163c78dc683f404d74583aba2f Mon Sep 17 00:00:00 2001 From: Jun Matsushita Date: Mon, 8 Sep 2014 17:17:34 +0200 Subject: [PATCH] Pushed ngraph pixi test with thousands of nodes. uf6/design#8 --- .gitignore | 1 + README.md | 19 + lib/fabric.js | 21283 ++++++++++++++++++++++++++++++ lib/pixi.dev.js | 14643 ++++++++++++++++++++ lib/pixi.js | 15 + pixi/README.md | 94 + pixi/assets/balancedBinTree.png | Bin 0 -> 101495 bytes pixi/assets/circularLadder.png | Bin 0 -> 16355 bytes pixi/assets/complete.png | Bin 0 -> 4306 bytes pixi/assets/create.js | 57 + pixi/assets/grid.png | Bin 0 -> 93814 bytes pixi/assets/ladder.png | Bin 0 -> 17744 bytes pixi/assets/path.png | Bin 0 -> 7784 bytes pixi/bundle.js | 3125 +++++ pixi/doItFromNode.js | 53 + pixi/index.html | 14 + pixi/index.js | 49 + pixi/lib/createLinkUI.js | 7 + pixi/lib/createNodeUI.js | 7 + pixi/lib/renderLink.js | 5 + pixi/lib/renderNode.js | 8 + pixi/package.json | 20 + pixi/ui.js | 55 + 23 files changed, 39455 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 lib/fabric.js create mode 100644 lib/pixi.dev.js create mode 100644 lib/pixi.js create mode 100644 pixi/README.md create mode 100644 pixi/assets/balancedBinTree.png create mode 100644 pixi/assets/circularLadder.png create mode 100644 pixi/assets/complete.png create mode 100644 pixi/assets/create.js create mode 100644 pixi/assets/grid.png create mode 100644 pixi/assets/ladder.png create mode 100644 pixi/assets/path.png create mode 100644 pixi/bundle.js create mode 100644 pixi/doItFromNode.js create mode 100644 pixi/index.html create mode 100644 pixi/index.js create mode 100644 pixi/lib/createLinkUI.js create mode 100644 pixi/lib/createNodeUI.js create mode 100644 pixi/lib/renderLink.js create mode 100644 pixi/lib/renderNode.js create mode 100644 pixi/package.json create mode 100644 pixi/ui.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..58c0865 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# Open Oil + +Create data file. + +curl -d @ngraph_cypher.json -H accept:application/json -H content-type:application/json http://localhost:7474/db/data/cypher | json data | json 0 | json 0 > ngraph_results.json + +Compile + +npm start + +# Fabric.js and ngraph + +This folder shows how to use [`ngraph.fabric`](https://github.com/anvaka/ngraph.fabric) as +a graph rendering engine. + +[![rendered from node.js](https://raw2.github.com/anvaka/ngraph.fabric/master/example/node.js/outGraph.png)](http://anvaka.github.io/ngraph/examples/fabric.js/Node%20and%20Browser/index.html) + +NB: Image above was [rendered from Node.js](https://github.com/anvaka/ngraph/tree/master/examples/fabric.js/Node%20and%20Browser). +Click on the image to see interactive version rendered by the same code in your browser. diff --git a/lib/fabric.js b/lib/fabric.js new file mode 100644 index 0000000..29e447b --- /dev/null +++ b/lib/fabric.js @@ -0,0 +1,21283 @@ +/* build: `node build.js modules=ALL exclude=gestures,cufon,json minifier=uglifyjs` */ +/*! Fabric.js Copyright 2008-2013, Printio (Juriy Zaytsev, Maxim Chernyak) */ + +var fabric = fabric || { version: "1.4.3" }; +if (typeof exports !== 'undefined') { + exports.fabric = fabric; +} + +if (typeof document !== 'undefined' && typeof window !== 'undefined') { + fabric.document = document; + fabric.window = window; +} +else { + // assume we're running under node.js when document/window are not present + fabric.document = require("jsdom") + .jsdom(""); + + fabric.window = fabric.document.createWindow(); +} + +/** + * True when in environment that supports touch events + * @type boolean + */ +fabric.isTouchSupported = "ontouchstart" in fabric.document.documentElement; + +/** + * True when in environment that's probably Node.js + * @type boolean + */ +fabric.isLikelyNode = typeof Buffer !== 'undefined' && + typeof window === 'undefined'; + + +/** + * Attributes parsed from all SVG elements + * @type array + */ +fabric.SHARED_ATTRIBUTES = [ + "transform", + "fill", "fill-opacity", "fill-rule", + "opacity", + "stroke", "stroke-dasharray", "stroke-linecap", + "stroke-linejoin", "stroke-miterlimit", + "stroke-opacity", "stroke-width" +]; + + +(function(){ + + /** + * @private + * @param {String} eventName + * @param {Function} handler + */ + function _removeEventListener(eventName, handler) { + if (!this.__eventListeners[eventName]) return; + + if (handler) { + fabric.util.removeFromArray(this.__eventListeners[eventName], handler); + } + else { + this.__eventListeners[eventName].length = 0; + } + } + + /** + * Observes specified event + * @deprecated `observe` deprecated since 0.8.34 (use `on` instead) + * @memberOf fabric.Observable + * @alias on + * @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler}) + * @param {Function} handler Function that receives a notification when an event of the specified type occurs + * @return {Self} thisArg + * @chainable + */ + function observe(eventName, handler) { + if (!this.__eventListeners) { + this.__eventListeners = { }; + } + // one object with key/value pairs was passed + if (arguments.length === 1) { + for (var prop in eventName) { + this.on(prop, eventName[prop]); + } + } + else { + if (!this.__eventListeners[eventName]) { + this.__eventListeners[eventName] = [ ]; + } + this.__eventListeners[eventName].push(handler); + } + return this; + } + + /** + * Stops event observing for a particular event handler. 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) { + this.__eventListeners = { }; + } + // 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++) { + // avoiding try/catch for perf. reasons + listenersForEvent[i].call(this, options || { }); + } + 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 = { + + /** + * Adds objects to collection, then renders canvas (if `renderOnAddRemove` is not `false`) + * Objects should be instances of (or inherit from) fabric.Object + * @param {...fabric.Object} object Zero or more fabric instances + * @return {Self} thisArg + */ + add: function () { + this._objects.push.apply(this._objects, arguments); + 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(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; + + 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) { + objects.splice(index, 1); + this._onObjectRemoved(arguments[i]); + } + } + + this.renderOnAddRemove && 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. + * Iteration happens in reverse order (for performance reasons). + * Callback is invoked in a context of Global Object (e.g. `window`) + * when no `context` argument is given + * + * @param {Object} context Context (aka thisObject) + * @return {Self} thisArg + */ + forEachObject: function(callback, context) { + var objects = this.getObjects(), + i = objects.length; + while (i--) { + callback.call(context, objects[i], i, objects); + } + return this; + }, + + /** + * Returns 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); + } +}; + + +(function(global) { + + var sqrt = Math.sqrt, + atan2 = Math.atan2, + 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 {Any} 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} The point to rotate + * @param {fabric.Point} The origin of the rotation + * @param {Number} The radians of the angle for the rotation + * @return {fabric.Point} The new rotated point + */ + rotatePoint: function(point, origin, radians) { + var sin = Math.sin(radians), + cos = Math.cos(radians); + + point.subtractEquals(origin); + + var rx = point.x * cos - point.y * sin, + ry = point.x * sin + point.y * cos; + + return new fabric.Point(rx, ry).addEquals(origin); + }, + + /** + * A wrapper around Number#toFixed, which contrary to native method returns number, not string. + * @static + * @memberOf fabric.util + * @param {Number | String} number number to operate on + * @param {Number} fractionDigits number of fraction digits to "leave" + * @return {Number} + */ + toFixed: function(number, fractionDigits) { + return parseFloat(Number(number).toFixed(fractionDigits)); + }, + + /** + * 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, + obj = global || fabric.window; + + for (var i = 0; i < len; ++i) { + obj = obj[parts[i]]; + } + + return obj; + }, + + /** + * Loads image element from given url and passes it to a callback + * @memberOf fabric.util + * @param {String} url URL representing an image + * @param {Function} callback Callback; invoked with loaded image + * @param {Any} [context] 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 && typeof crossOrigin !== 'undefined') { + 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 {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; + + 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); + if (klass.async) { + klass.fromObject(o, function (obj, error) { + if (!error) { + enlivenedObjects[index] = obj; + reviver && reviver(o, enlivenedObjects[index]); + } + onLoaded(); + }); + } + else { + enlivenedObjects[index] = klass.fromObject(o); + reviver && reviver(o, enlivenedObjects[index]); + onLoaded(); + } + }); + }, + + /** + * Groups SVG elements (usually those retrieved from SVG document) + * @static + * @memberOf fabric.util + * @param {Array} elements SVG elements to group + * @param {Object} [options] Options object + * @return {fabric.Object|fabric.PathGroup} + */ + groupSVGElements: function(elements, options, path) { + var object; + + if (elements.length > 1) { + object = new fabric.PathGroup(elements, options); + } + else { + object = elements[0]; + } + + if (typeof path !== 'undefined') { + object.setSourcePath(path); + } + return object; + }, + + /** + * Populates an object with properties of another object + * @static + * @memberOf fabric.util + * @param {Object} source Source object + * @param {Object} destination Destination object + * @return {Array} properties Propertie names to include + */ + 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')); + if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') { + G_vmlCanvasManager.initElement(canvasEl); + } + return canvasEl; + }, + + /** + * Creates image element (works on client and node) + * @static + * @memberOf fabric.util + * @return {HTMLImageElement} HTML image element + */ + 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; + + for (var i = proto.stateProperties.length; i--; ) { + + var propName = proto.stateProperties[i], + capitalizedPropName = propName.charAt(0).toUpperCase() + propName.slice(1), + setterName = 'set' + capitalizedPropName, + getterName = 'get' + capitalizedPropName; + + // using `new Function` for better introspection + if (!proto[getterName]) { + proto[getterName] = (function(property) { + return new Function('return this.get("' + property + '")'); + })(propName); + } + if (!proto[setterName]) { + proto[setterName] = (function(property) { + return new Function('value', 'return this.set("' + property + '", value)'); + })(propName); + } + } + }, + + /** + * @static + * @memberOf fabric.util + * @param {fabric.Object} receiver Object implementing `clipTo` method + * @param {CanvasRenderingContext2D} ctx Context to clip + */ + 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} matrixA First transformMatrix + * @param {Array} matrixB Second transformMatrix + * @return {Array} The product of the two transform matrices + */ + multiplyTransformMatrices: function(matrixA, matrixB) { + // Matrix multiply matrixA * matrixB + var a = [ + [matrixA[0], matrixA[2], matrixA[4]], + [matrixA[1], matrixA[3], matrixA[5]], + [0 , 0 , 1 ] + ]; + + var b = [ + [matrixB[0], matrixB[2], matrixB[4]], + [matrixB[1], matrixB[3], matrixB[5]], + [0 , 0 , 1 ] + ]; + + var result = []; + for (var r=0; r<3; r++) { + result[r] = []; + for (var c=0; c<3; c++) { + var sum = 0; + for (var k=0; k<3; k++) { + sum += a[r][k]*b[k][c]; + } + + result[r][c] = sum; + } + } + + return [ + result[0][0], + result[1][0], + result[0][1], + result[1][1], + result[0][2], + result[1][2] + ]; + }, + + /** + * 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]; + }, + + /** + * Normalizes polygon/polyline points according to their dimensions + * @param {Array} points + * @param {Object} options + */ + normalizePoints: function(points, options) { + var minX = fabric.util.array.min(points, 'x'), + minY = fabric.util.array.min(points, 'y'); + + minX = minX < 0 ? minX : 0; + minY = minX < 0 ? minY : 0; + + for (var i = 0, len = points.length; i < len; i++) { + // normalize coordinates, according to containing box + // (dimensions of which are passed via `options`) + points[i].x -= (options.width / 2 + minX) || 0; + points[i].y -= (options.height / 2 + minY) || 0; + } + }, + + /** + * 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; + var imageData = ctx.getImageData( + x, y, (tolerance * 2) || 1, (tolerance * 2) || 1); + + // Split image data - for tolerance > 1, pixelDataSize = 4; + for (var i = 3, l = imageData.data.length; i < l; i += 4) { + var temp = imageData.data[i]; + _isTransparent = temp <= 0; + if (_isTransparent === false) break; // Stop if colour found + } + + imageData = null; + + return _isTransparent; + } + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + + var arcToSegmentsCache = { }, + segmentToBezierCache = { }, + _join = Array.prototype.join, + argsString; + + // Generous contribution by Raph Levien, from libsvg-0.1.0.tar.gz + function arcToSegments(x, y, rx, ry, large, sweep, rotateX, ox, oy) { + + argsString = _join.call(arguments); + + if (arcToSegmentsCache[argsString]) { + return arcToSegmentsCache[argsString]; + } + + var coords = getXYCoords(rotateX, rx, ry, ox, oy, x, y); + + var d = (coords.x1-coords.x0) * (coords.x1-coords.x0) + + (coords.y1-coords.y0) * (coords.y1-coords.y0); + + var sfactor_sq = 1 / d - 0.25; + if (sfactor_sq < 0) sfactor_sq = 0; + + var sfactor = Math.sqrt(sfactor_sq); + if (sweep === large) sfactor = -sfactor; + + var xc = 0.5 * (coords.x0 + coords.x1) - sfactor * (coords.y1-coords.y0); + var yc = 0.5 * (coords.y0 + coords.y1) + sfactor * (coords.x1-coords.x0); + + var th0 = Math.atan2(coords.y0-yc, coords.x0-xc); + var th1 = Math.atan2(coords.y1-yc, coords.x1-xc); + + var th_arc = th1-th0; + if (th_arc < 0 && sweep === 1) { + th_arc += 2*Math.PI; + } + else if (th_arc > 0 && sweep === 0) { + th_arc -= 2 * Math.PI; + } + + var segments = Math.ceil(Math.abs(th_arc / (Math.PI * 0.5 + 0.001))); + var result = []; + for (var i=0; i 1) { + pl = Math.sqrt(pl); + rx *= pl; + ry *= pl; + } + + var a00 = cos_th / rx; + var a01 = sin_th / rx; + var a10 = (-sin_th) / ry; + var a11 = (cos_th) / ry; + + return { + x0: a00 * ox + a01 * oy, + y0: a10 * ox + a11 * oy, + x1: a00 * x + a01 * y, + y1: a10 * x + a11 * y, + sin_th: sin_th, + cos_th: cos_th + }; + } + + function segmentToBezier(cx, cy, th0, th1, rx, ry, sin_th, cos_th) { + argsString = _join.call(arguments); + if (segmentToBezierCache[argsString]) { + return segmentToBezierCache[argsString]; + } + + var a00 = cos_th * rx; + var a01 = -sin_th * ry; + var a10 = sin_th * rx; + var a11 = cos_th * ry; + + var th_half = 0.5 * (th1 - th0); + var t = (8/3) * Math.sin(th_half * 0.5) * + Math.sin(th_half * 0.5) / Math.sin(th_half); + + var x1 = cx + Math.cos(th0) - t * Math.sin(th0); + var y1 = cy + Math.sin(th0) + t * Math.cos(th0); + var x3 = cx + Math.cos(th1); + var y3 = cy + Math.sin(th1); + var x2 = x3 + t * Math.sin(th1); + var y2 = y3 - t * Math.cos(th1); + + segmentToBezierCache[argsString] = [ + a00 * x1 + a01 * y1, a10 * x1 + a11 * y1, + a00 * x2 + a01 * y2, a10 * x2 + a11 * y2, + a00 * x3 + a01 * y3, a10 * x3 + a11 * y3 + ]; + + return segmentToBezierCache[argsString]; + } + + /** + * Draws arc + * @param {CanvasRenderingContext2D} ctx + * @param {Number} x + * @param {Number} y + * @param {Array} coords + */ + fabric.util.drawArc = function(ctx, x, y, coords) { + var rx = coords[0]; + var ry = coords[1]; + var rot = coords[2]; + var large = coords[3]; + var sweep = coords[4]; + var ex = coords[5]; + var ey = coords[6]; + var segs = arcToSegments(ex, ey, rx, ry, large, sweep, rot, x, y); + for (var i=0; i>> 0; + if (len === 0) { + return -1; + } + var n = 0; + if (arguments.length > 0) { + n = Number(arguments[1]); + if (n !== n) { // shortcut for verifying if it's NaN + n = 0; + } + else if (n !== 0 && n !== Number.POSITIVE_INFINITY && n !== Number.NEGATIVE_INFINITY) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + } + if (n >= len) { + return -1; + } + var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); + for (; k < len; k++) { + if (k in t && t[k] === searchElement) { + return k; + } + } + return -1; + }; + } + + if (!Array.prototype.forEach) { + /** + * Iterates an array, invoking callback for each element + * @param {Function} fn Callback to invoke for each element + * @param {Object} [context] Context to invoke callback in + * @return {Array} + */ + Array.prototype.forEach = function(fn, context) { + for (var i = 0, len = this.length >>> 0; i < len; i++) { + if (i in this) { + fn.call(context, this[i], i, this); + } + } + }; + } + + if (!Array.prototype.map) { + /** + * Returns a result of iterating over an array, invoking callback for each element + * @param {Function} fn Callback to invoke for each element + * @param {Object} [context] Context to invoke callback in + * @return {Array} + */ + Array.prototype.map = function(fn, context) { + var result = [ ]; + for (var i = 0, len = this.length >>> 0; i < len; i++) { + if (i in this) { + result[i] = fn.call(context, this[i], i, this); + } + } + return result; + }; + } + + if (!Array.prototype.every) { + /** + * Returns true if a callback returns truthy value for all elements in an array + * @param {Function} fn Callback to invoke for each element + * @param {Object} [context] Context to invoke callback in + * @return {Boolean} + */ + Array.prototype.every = function(fn, context) { + for (var i = 0, len = this.length >>> 0; i < len; i++) { + if (i in this && !fn.call(context, this[i], i, this)) { + return false; + } + } + return true; + }; + } + + if (!Array.prototype.some) { + /** + * Returns true if a callback returns truthy value for at least one element in an array + * @param {Function} fn Callback to invoke for each element + * @param {Object} [context] Context to invoke callback in + * @return {Boolean} + */ + Array.prototype.some = function(fn, context) { + for (var i = 0, len = this.length >>> 0; i < len; i++) { + if (i in this && fn.call(context, this[i], i, this)) { + return true; + } + } + return false; + }; + } + + if (!Array.prototype.filter) { + /** + * Returns the result of iterating over elements in an array + * @param {Function} fn Callback to invoke for each element + * @param {Object} [context] Context to invoke callback in + * @return {Array} + */ + Array.prototype.filter = function(fn, context) { + var result = [ ], val; + for (var i = 0, len = this.length >>> 0; i < len; i++) { + if (i in this) { + val = this[i]; // in case fn mutates this + if (fn.call(context, val, i, this)) { + result.push(val); + } + } + } + return result; + }; + } + + if (!Array.prototype.reduce) { + /** + * Returns "folded" (reduced) result of iterating over elements in an array + * @param {Function} fn Callback to invoke for each element + * @param {Object} [context] Context to invoke callback in + * @return {Any} + */ + Array.prototype.reduce = function(fn /*, initial*/) { + var len = this.length >>> 0, + i = 0, + rv; + + if (arguments.length > 1) { + rv = arguments[1]; + } + else { + do { + if (i in this) { + rv = this[i++]; + break; + } + // if array contains no values, no initial value to return + if (++i >= len) { + throw new TypeError(); + } + } + while (true); + } + for (; i < len; i++) { + if (i in this) { + rv = fn.call(null, rv, this[i], i, this); + } + } + return rv; + }; + } + + /* _ES5_COMPAT_END_ */ + + /** + * Invokes method on all items in a given array + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} method Name of a method to invoke + * @return {Array} + */ + function invoke(array, method) { + var args = slice.call(arguments, 2), result = [ ]; + for (var i = 0, len = array.length; i < len; i++) { + result[i] = args.length ? array[i][method].apply(array[i], args) : array[i][method].call(array[i]); + } + return result; + } + + /** + * Finds maximum value in array (not necessarily "first" one) + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} byProperty + * @return {Any} + */ + function max(array, byProperty) { + 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 {Any} + */ + function min(array, byProperty) { + return find(array, byProperty, function(value1, value2) { + return value1 < value2; + }); + } + + /** + * @private + */ + function find(array, byProperty, condition) { + if (!array || array.length === 0) return undefined; + + 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 = { + invoke: invoke, + min: min, + max: max + }; + +})(); + + +(function(){ + + /** + * Copies all enumerable properties of one object to another + * @memberOf fabric.util.object + * @param {Object} destination Where to copy to + * @param {Object} source Where to copy from + * @return {Object} + */ + function extend(destination, source) { + // JScript DontEnum bug is not taken care of + for (var property in source) { + destination[property] = source[property]; + } + return destination; + } + + /** + * Creates an empty object and copies all enumerable properties of another object to it + * @memberOf fabric.util.object + * @param {Object} object Object to clone + * @return {Object} + */ + function clone(object) { + return extend({ }, object); + } + + /** @namespace fabric.util.object */ + fabric.util.object = { + extend: extend, + clone: clone + }; + +})(); + + +(function() { + +/* _ES5_COMPAT_START_ */ +if (!String.prototype.trim) { + /** + * Trims a string (removing whitespace from the beginning and the end) + * @function external:String#trim + * @see String#trim on MDN + */ + String.prototype.trim = function () { + // this trim is not fully ES3 or ES5 compliant, but it should cover most cases for now + return this.replace(/^[\s\xA0]+/, '').replace(/[\s\xA0]+$/, ''); + }; +} +/* _ES5_COMPAT_END_ */ + +/** + * Camelizes a string + * @memberOf fabric.util.string + * @param {String} string String to camelize + * @return {String} Camelized version of a string + */ +function camelize(string) { + return string.replace(/-+(.)?/g, function(match, character) { + return character ? character.toUpperCase() : ''; + }); +} + +/** + * Capitalizes a string + * @memberOf fabric.util.string + * @param {String} string String to capitalize + * @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 fn = this, args = slice.call(arguments, 1), bound; + if (args.length) { + bound = function() { + return apply.call(fn, this instanceof Dummy ? this : thisArg, args.concat(slice.call(arguments))); + }; + } + else { + /** @ignore */ + bound = function() { + return apply.call(fn, this instanceof Dummy ? this : thisArg, arguments); + }; + } + Dummy.prototype = this.prototype; + bound.prototype = new Dummy(); + + return bound; + }; + } + +})(); +/* _ES5_COMPAT_END_ */ + + +(function() { + + var slice = Array.prototype.slice, emptyFunction = function() { }; + + var IS_DONTENUM_BUGGY = (function(){ + for (var p in { toString: 1 }) { + if (p === 'toString') return false; + } + return true; + })(); + + /** @ignore */ + var addMethods = function(klass, source, parent) { + for (var property in source) { + + if (property in klass.prototype && + typeof klass.prototype[property] === 'function' && + (source[property] + '').indexOf('callSuper') > -1) { + + klass.prototype[property] = (function(property) { + return function() { + + var superclass = this.constructor.superclass; + this.constructor.superclass = parent; + var returnValue = source[property].apply(this, arguments); + this.constructor.superclass = superclass; + + if (property !== 'initialize') { + return returnValue; + } + }; + })(property); + } + else { + klass.prototype[property] = source[property]; + } + + if (IS_DONTENUM_BUGGY) { + if (source.toString !== Object.prototype.toString) { + klass.prototype.toString = source.toString; + } + if (source.valueOf !== Object.prototype.valueOf) { + klass.prototype.valueOf = source.valueOf; + } + } + } + }; + + function Subclass() { } + + function callSuper(methodName) { + var fn = this.constructor.superclass.prototype[methodName]; + return (arguments.length > 1) + ? fn.apply(this, slice.call(arguments, 1)) + : fn.call(this); + } + + /** + * Helper for creation of "classes". + * @memberOf fabric.util + * @param parent optional "Class" to inherit from + * @param properties Properties shared by all instances of this class + * (be careful modifying objects defined here as this would affect all instances) + */ + function createClass() { + var parent = null, + properties = slice.call(arguments, 0); + + if (typeof properties[0] === 'function') { + parent = properties.shift(); + } + function klass() { + this.initialize.apply(this, arguments); + } + + klass.superclass = parent; + klass.subclasses = [ ]; + + if (parent) { + Subclass.prototype = parent.prototype; + klass.prototype = new Subclass(); + parent.subclasses.push(klass); + } + for (var i = 0, length = properties.length; i < length; i++) { + addMethods(klass, properties[i], parent); + } + if (!klass.prototype.initialize) { + klass.prototype.initialize = emptyFunction; + } + klass.prototype.constructor = klass; + klass.prototype.callSuper = callSuper; + return klass; + } + + fabric.util.createClass = createClass; +})(); + + +(function () { + + 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; + } + var getUniqueId = (function () { + var uid = 0; + return function (element) { + return element.__uniqueID || (element.__uniqueID = 'uniqueID__' + uid++); + }; + })(); + + /** @ignore */ + var getElement, setElement; + + (function () { + var elements = { }; + /** @ignore */ + getElement = function (uid) { + return elements[uid]; + }; + /** @ignore */ + setElement = function (uid, element) { + elements[uid] = element; + }; + })(); + + function createListener(uid, handler) { + return { + handler: handler, + wrappedHandler: createWrappedHandler(uid, handler) + }; + } + + function createWrappedHandler(uid, handler) { + return function (e) { + handler.call(getElement(uid), e || fabric.window.event); + }; + } + + function createDispatcher(uid, eventName) { + return function (e) { + if (handlers[uid] && handlers[uid][eventName]) { + var handlersForEvent = handlers[uid][eventName]; + for (var i = 0, len = handlersForEvent.length; i < len; i++) { + handlersForEvent[i].call(this, e || fabric.window.event); + } + } + }; + } + + var shouldUseAddListenerRemoveListener = ( + areHostMethods(fabric.document.documentElement, 'addEventListener', 'removeEventListener') && + areHostMethods(fabric.window, 'addEventListener', 'removeEventListener')), + + shouldUseAttachEventDetachEvent = ( + areHostMethods(fabric.document.documentElement, 'attachEvent', 'detachEvent') && + areHostMethods(fabric.window, 'attachEvent', 'detachEvent')), + + // IE branch + listeners = { }, + + // DOM L0 branch + handlers = { }, + + addListener, removeListener; + + if (shouldUseAddListenerRemoveListener) { + /** @ignore */ + addListener = function (element, eventName, handler) { + element.addEventListener(eventName, handler, false); + }; + /** @ignore */ + removeListener = function (element, eventName, handler) { + element.removeEventListener(eventName, handler, false); + }; + } + + else if (shouldUseAttachEventDetachEvent) { + /** @ignore */ + addListener = function (element, eventName, handler) { + var uid = getUniqueId(element); + setElement(uid, element); + if (!listeners[uid]) { + listeners[uid] = { }; + } + if (!listeners[uid][eventName]) { + listeners[uid][eventName] = [ ]; + + } + var listener = createListener(uid, handler); + listeners[uid][eventName].push(listener); + element.attachEvent('on' + eventName, listener.wrappedHandler); + }; + /** @ignore */ + removeListener = function (element, eventName, handler) { + var uid = getUniqueId(element), listener; + if (listeners[uid] && listeners[uid][eventName]) { + for (var i = 0, len = listeners[uid][eventName].length; i < len; i++) { + listener = listeners[uid][eventName][i]; + if (listener && listener.handler === handler) { + element.detachEvent('on' + eventName, listener.wrappedHandler); + listeners[uid][eventName][i] = null; + } + } + } + }; + } + else { + /** @ignore */ + addListener = function (element, eventName, handler) { + var uid = getUniqueId(element); + if (!handlers[uid]) { + handlers[uid] = { }; + } + if (!handlers[uid][eventName]) { + handlers[uid][eventName] = [ ]; + var existingHandler = element['on' + eventName]; + if (existingHandler) { + handlers[uid][eventName].push(existingHandler); + } + element['on' + eventName] = createDispatcher(uid, eventName); + } + handlers[uid][eventName].push(handler); + }; + /** @ignore */ + removeListener = function (element, eventName, handler) { + var uid = getUniqueId(element); + if (handlers[uid] && handlers[uid][eventName]) { + var handlersForEvent = handlers[uid][eventName]; + for (var i = 0, len = handlersForEvent.length; i < len; i++) { + if (handlersForEvent[i] === handler) { + handlersForEvent.splice(i, 1); + } + } + } + }; + } + + /** + * Adds an event listener to an element + * @function + * @memberOf fabric.util + * @param {HTMLElement} element + * @param {String} eventName + * @param {Function} handler + */ + fabric.util.addListener = addListener; + + /** + * Removes an event listener from an element + * @function + * @memberOf fabric.util + * @param {HTMLElement} element + * @param {String} eventName + * @param {Function} handler + */ + fabric.util.removeListener = removeListener; + + /** + * Cross-browser wrapper for getting event's coordinates + * @memberOf fabric.util + * @param {Event} event Event object + * @param {HTMLCanvasElement} upperCanvasEl <canvas> element on which object selection is drawn + */ + function getPointer(event, upperCanvasEl) { + event || (event = fabric.window.event); + + var element = event.target || + (typeof event.srcElement !== unknown ? event.srcElement : null); + + var scroll = fabric.util.getScrollLeftTop(element, upperCanvasEl); + + 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); + }; + + var 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; + } + + /** + * Converts an array-like object (e.g. arguments or NodeList) to an array + * @memberOf fabric.util + * @param {Object} arrayLike + * @return {Array} + */ + var toArray = function(arrayLike) { + return _slice.call(arrayLike, 0); + }; + + var sliceCanConvertNodelists; + try { + sliceCanConvertNodelists = toArray(fabric.document.childNodes) instanceof Array; + } + catch(err) { } + + if (!sliceCanConvertNodelists) { + toArray = function(arrayLike) { + var arr = new Array(arrayLike.length), i = arrayLike.length; + while (i--) { + arr[i] = arrayLike[i]; + } + return arr; + }; + } + + /** + * Creates specified element with specified attributes + * @memberOf fabric.util + * @param {String} tagName Type of an element to create + * @param {Object} [attributes] Attributes to set on an element + * @return {HTMLElement} Newly created element + */ + function makeElement(tagName, attributes) { + var el = fabric.document.createElement(tagName); + for (var prop in attributes) { + if (prop === 'class') { + el.className = attributes[prop]; + } + else if (prop === 'for') { + el.htmlFor = attributes[prop]; + } + else { + el.setAttribute(prop, attributes[prop]); + } + } + return el; + } + + /** + * Adds class to an element + * @memberOf fabric.util + * @param {HTMLElement} element Element to add class to + * @param {String} className Class to add to an element + */ + function addClass(element, className) { + if ((' ' + element.className + ' ').indexOf(' ' + className + ' ') === -1) { + element.className += (element.className ? ' ' : '') + className; + } + } + + /** + * Wraps element with another element + * @memberOf fabric.util + * @param {HTMLElement} element Element to wrap + * @param {HTMLElement|String} wrapper Element to wrap with + * @param {Object} [attributes] Attributes to set on a wrapper + * @return {HTMLElement} wrapper + */ + function wrapElement(element, wrapper, attributes) { + if (typeof wrapper === 'string') { + wrapper = makeElement(wrapper, attributes); + } + if (element.parentNode) { + element.parentNode.replaceChild(wrapper, element); + } + wrapper.appendChild(element); + return wrapper; + } + + function getScrollLeftTop(element, upperCanvasEl) { + + var firstFixedAncestor, + origElement, + left = 0, + top = 0, + docElement = fabric.document.documentElement, + body = fabric.document.body || { + scrollLeft: 0, scrollTop: 0 + }; + + origElement = element; + + while (element && element.parentNode && !firstFixedAncestor) { + + element = element.parentNode; + + if (element !== fabric.document && + fabric.util.getElementStyle(element, 'position') === 'fixed') { + firstFixedAncestor = element; + } + + if (element !== fabric.document && + origElement !== upperCanvasEl && + fabric.util.getElementStyle(element, 'position') === 'absolute') { + left = 0; + top = 0; + } + else 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; + } + } + + 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, + box = {left: 0, top: 0}, + doc = element && element.ownerDocument, + offset = {left: 0, top: 0}, + scrollLeftTop, + offsetAttributes = { + 'borderLeftWidth': 'left', + 'borderTopWidth': 'top', + 'paddingLeft': 'left', + 'paddingTop': 'top' + }; + + if (!doc){ + return {left: 0, top: 0}; + } + + for (var attr in offsetAttributes) { + offset[offsetAttributes[attr]] += parseInt(getElementStyle(element, attr), 10) || 0; + } + + docElem = doc.documentElement; + if ( typeof element.getBoundingClientRect !== "undefined" ) { + box = element.getBoundingClientRect(); + } + + scrollLeftTop = fabric.util.getScrollLeftTop(element, null); + + 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. + */ + function getElementStyle(element, attr) { + if (!element.style) { + element.style = { }; + } + + if (fabric.document.defaultView && fabric.document.defaultView.getComputedStyle) { + return fabric.document.defaultView.getComputedStyle(element, null)[attr]; + } + else { + var value = element.style[attr]; + if (!value && element.currentStyle) value = element.currentStyle[attr]; + return value; + } + } + + (function () { + var style = fabric.document.documentElement.style; + + var selectProp = 'userSelect' in style + ? 'userSelect' + : 'MozUserSelect' in style + ? 'MozUserSelect' + : 'WebkitUserSelect' in style + ? 'WebkitUserSelect' + : 'KhtmlUserSelect' in style + ? 'KhtmlUserSelect' + : ''; + + /** + * Makes element unselectable + * @memberOf fabric.util + * @param {HTMLElement} element Element to make unselectable + * @return {HTMLElement} Element that was passed in + */ + function makeElementUnselectable(element) { + if (typeof element.onselectstart !== 'undefined') { + element.onselectstart = fabric.util.falseFunction; + } + if (selectProp) { + element.style[selectProp] = 'none'; + } + else if (typeof element.unselectable === 'string') { + element.unselectable = 'on'; + } + return element; + } + + /** + * Makes element selectable + * @memberOf fabric.util + * @param {HTMLElement} element Element to make selectable + * @return {HTMLElement} Element that was passed in + */ + function makeElementSelectable(element) { + if (typeof element.onselectstart !== 'undefined') { + element.onselectstart = null; + } + if (selectProp) { + element.style[selectProp] = ''; + } + else if (typeof element.unselectable === 'string') { + element.unselectable = ''; + } + return element; + } + + fabric.util.makeElementUnselectable = makeElementUnselectable; + fabric.util.makeElementSelectable = makeElementSelectable; + })(); + + (function() { + + /** + * Inserts a script element with a given url into a document; invokes callback, when that script is finished loading + * @memberOf fabric.util + * @param {String} url URL of a script to load + * @param {Function} callback Callback to execute when script is finished loading + */ + function getScript(url, callback) { + var headEl = fabric.document.getElementsByTagName("head")[0], + scriptEl = fabric.document.createElement('script'), + loading = true; + + /** @ignore */ + scriptEl.onload = /** @ignore */ scriptEl.onreadystatechange = function(e) { + if (loading) { + if (typeof this.readyState === 'string' && + this.readyState !== 'loaded' && + this.readyState !== 'complete') return; + loading = false; + callback(e || fabric.window.event); + scriptEl = scriptEl.onload = scriptEl.onreadystatechange = null; + } + }; + scriptEl.src = url; + headEl.appendChild(scriptEl); + // causes issue in Opera + // headEl.removeChild(scriptEl); + } + + fabric.util.getScript = getScript; + })(); + + fabric.util.getById = getById; + fabric.util.toArray = toArray; + fabric.util.makeElement = makeElement; + fabric.util.addClass = addClass; + fabric.util.wrapElement = wrapElement; + fabric.util.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 {Function} options.onComplete Callback to invoke when request is completed + * @return {XMLHttpRequest} request + */ + function request(url, options) { + + options || (options = { }); + + var method = options.method ? options.method.toUpperCase() : 'GET', + onComplete = options.onComplete || function() { }, + xhr = makeXHR(), + body; + + /** @ignore */ + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + onComplete(xhr); + xhr.onreadystatechange = emptyFn; + } + }; + + if (method === 'GET') { + body = null; + if (typeof options.parameters === 'string') { + url = addParamToUrl(url, options.parameters); + } + } + + xhr.open(method, url, true); + + if (method === 'POST' || method === 'PUT') { + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + } + + xhr.send(body); + return xhr; + } + + fabric.util.request = request; +})(); + + +/** + * Wrapper around `console.log` (when available) + * @param {Any} values Values to log + */ +fabric.log = function() { }; + +/** + * Wrapper around `console.warn` (when available) + * @param {Any} Values to log as a warning + */ +fabric.warn = function() { }; + +if (typeof console !== 'undefined') { + ['log', 'warn'].forEach(function(methodName) { + if (typeof console[methodName] !== 'undefined' && console[methodName].apply) { + fabric[methodName] = function() { + return console[methodName].apply(console, arguments); + }; + } + }); +} + + +(function() { + + /** + * Changes value from one to another within certain period of time, invoking callbacks as value is being changed. + * @memberOf fabric.util + * @param {Object} [options] Animation options + * @param {Function} [options.onChange] Callback; invoked on every value change + * @param {Function} [options.onComplete] Callback; invoked when value change is completed + * @param {Number} [options.startValue=0] Starting value + * @param {Number} [options.endValue=100] Ending value + * @param {Number} [options.byValue=100] Value to modify the property by + * @param {Function} [options.easing] Easing function + * @param {Number} [options.duration=500] Duration of change + */ + function animate(options) { + + 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 + */ + var requestAnimFrame = function() { + return _requestAnimFrame.apply(fabric.window, arguments); + }; + + fabric.util.animate = animate; + fabric.util.requestAnimFrame = requestAnimFrame; + +})(); + + +(function() { + + function normalize(a, c, p, s) { + if (a < Math.abs(c)) { a=c; s=p/4; } + 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;var p=0;var a=c; + if (t===0) return b; + t /= d; + if (t===1) return b+c; + if (!p) p=d*0.3; + 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;var p=0;var a=c; + if (t===0) return b; + t /= d; + if (t===1) return b+c; + if (!p) p=d*0.3; + 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;var p=0;var a=c; + if (t===0) return b; + t /= d/2; + if (t===2) return b+c; + if (!p) p=d*(0.3*1.5); + 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, + capitalize = fabric.util.string.capitalize, + clone = fabric.util.object.clone, + toFixed = fabric.util.toFixed, + multiplyTransformMatrices = fabric.util.multiplyTransformMatrices; + + var attributesMap = { + 'fill-opacity': 'fillOpacity', + 'fill-rule': 'fillRule', + 'font-family': 'fontFamily', + 'font-size': 'fontSize', + 'font-style': 'fontStyle', + 'font-weight': 'fontWeight', + 'cx': 'left', + 'x': 'left', + 'r': 'radius', + 'stroke-dasharray': 'strokeDashArray', + 'stroke-linecap': 'strokeLineCap', + 'stroke-linejoin': 'strokeLineJoin', + 'stroke-miterlimit':'strokeMiterLimit', + 'stroke-opacity': 'strokeOpacity', + 'stroke-width': 'strokeWidth', + 'text-decoration': 'textDecoration', + 'cy': 'top', + 'y': 'top', + 'transform': 'transformMatrix' + }; + + var colorAttributes = { + 'stroke': 'strokeOpacity', + 'fill': 'fillOpacity' + }; + + function normalizeAttr(attr) { + // transform attribute names + if (attr in attributesMap) { + return attributesMap[attr]; + } + return attr; + } + + function normalizeValue(attr, value, parentAttributes) { + var isArray; + + if ((attr === 'fill' || attr === 'stroke') && value === 'none') { + value = ''; + } + else if (attr === 'fillRule') { + value = (value === 'evenodd') ? 'destination-over' : value; + } + else if (attr === 'strokeDashArray') { + value = value.replace(/,/g, ' ').split(/\s+/); + } + else if (attr === 'transformMatrix') { + if (parentAttributes && parentAttributes.transformMatrix) { + value = multiplyTransformMatrices( + parentAttributes.transformMatrix, fabric.parseTransformAttribute(value)); + } + else { + value = fabric.parseTransformAttribute(value); + } + } + + isArray = Object.prototype.toString.call(value) === '[object Array]'; + + // TODO: need to normalize em, %, pt, etc. to px (!) + var parsed = isArray ? value.map(parseFloat) : parseFloat(value); + + return (!isArray && isNaN(parsed) ? value : parsed); + } + + /** + * @private + * @param {Object} attributes Array of attributes to parse + */ + function _setStrokeFillOpacity(attributes) { + for (var attr in colorAttributes) { + + if (!attributes[attr] || typeof attributes[colorAttributes[attr]] === 'undefined') continue; + + if (attributes[attr].indexOf('url(') === 0) continue; + + var color = new fabric.Color(attributes[attr]); + attributes[attr] = color.setAlpha(toFixed(color.getAlpha() * attributes[colorAttributes[attr]], 2)).toRgba(); + + delete attributes[colorAttributes[attr]]; + } + return attributes; + } + + /** + * 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 angle = args[0]; + + matrix[0] = Math.cos(angle); + matrix[1] = Math.sin(angle); + matrix[2] = -Math.sin(angle); + matrix[3] = Math.cos(angle); + } + + function scaleMatrix(matrix, args) { + var multiplierX = args[0], + multiplierY = (args.length === 2) ? args[1] : args[0]; + + matrix[0] = multiplierX; + matrix[3] = multiplierY; + } + + function skewXMatrix(matrix, args) { + matrix[2] = args[0]; + } + + function skewYMatrix(matrix, args) { + matrix[1] = args[0]; + } + + function translateMatrix(matrix, args) { + matrix[4] = args[0]; + if (args.length === 2) { + matrix[5] = args[1]; + } + } + + // identity matrix + var iMatrix = [ + 1, // a + 0, // b + 0, // c + 1, // d + 0, // e + 0 // f + ], + + // == begin transform regexp + number = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)', + + comma_wsp = '(?:\\s+,?\\s*|,\\s*)', + + skewX = '(?:(skewX)\\s*\\(\\s*(' + number + ')\\s*\\))', + + skewY = '(?:(skewY)\\s*\\(\\s*(' + number + ')\\s*\\))', + + rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + + comma_wsp + '(' + number + ')' + + comma_wsp + '(' + number + '))?\\s*\\))', + + scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + + comma_wsp + '(' + number + '))?\\s*\\))', + + translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + + comma_wsp + '(' + number + '))?\\s*\\))', + + matrix = '(?:(matrix)\\s*\\(\\s*' + + '(' + number + ')' + comma_wsp + + '(' + number + ')' + comma_wsp + + '(' + number + ')' + comma_wsp + + '(' + number + ')' + comma_wsp + + '(' + number + ')' + comma_wsp + + '(' + number + ')' + + '\\s*\\))', + + transform = '(?:' + + matrix + '|' + + translate + '|' + + scale + '|' + + rotate + '|' + + skewX + '|' + + skewY + + ')', + + transforms = '(?:' + transform + '(?:' + comma_wsp + transform + ')*' + ')', + + transform_list = '^\\s*(?:' + transforms + '?)\\s*$', + + // http://www.w3.org/TR/SVG/coords.html#TransformAttribute + reTransformList = new RegExp(transform_list), + // == end transform regexp + + reTransform = new RegExp(transform, 'g'); + + return function(attributeValue) { + + // start with identity matrix + var matrix = iMatrix.concat(); + var matrices = [ ]; + + // return if no argument was given or + // an argument does not match transform attribute regexp + if (!attributeValue || (attributeValue && !reTransformList.test(attributeValue))) { + return matrix; + } + + attributeValue.replace(reTransform, function(match) { + + var m = new RegExp(transform).exec(match).filter(function (match) { + return (match !== '' && match != null); + }), + operation = m[1], + args = m.slice(2).map(parseFloat); + + switch(operation) { + case 'translate': + translateMatrix(matrix, args); + break; + case 'rotate': + rotateMatrix(matrix, args); + break; + case 'scale': + scaleMatrix(matrix, args); + break; + case 'skewX': + skewXMatrix(matrix, args); + break; + case 'skewY': + skewYMatrix(matrix, args); + break; + case 'matrix': + matrix = args; + break; + } + + // snapshot current matrix into matrices array + matrices.push(matrix.concat()); + // reset + matrix = iMatrix.concat(); + }); + + var combinedMatrix = matrices[0]; + while (matrices.length > 1) { + matrices.shift(); + combinedMatrix = fabric.util.multiplyTransformMatrices(combinedMatrix, matrices[0]); + } + return combinedMatrix; + }; + })(); + + function parseFontDeclaration(value, oStyle) { + + // TODO: support non-px font size + var match = value.match(/(normal|italic)?\s*(normal|small-caps)?\s*(normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900)?\s*(\d+)px(?:\/(normal|[\d\.]+))?\s+(.*)/); + + if (!match) return; + + var fontStyle = match[1]; + // Font variant is not used + // var fontVariant = match[2]; + var fontWeight = match[3]; + var fontSize = match[4]; + var lineHeight = match[5]; + var fontFamily = match[6]; + + if (fontStyle) { + oStyle.fontStyle = fontStyle; + } + if (fontWeight) { + oStyle.fontSize = isNaN(parseFloat(fontWeight)) ? fontWeight : parseFloat(fontWeight); + } + if (fontSize) { + oStyle.fontSize = parseFloat(fontSize); + } + if (fontFamily) { + oStyle.fontFamily = fontFamily; + } + if (lineHeight) { + oStyle.lineHeight = lineHeight === 'normal' ? 1 : lineHeight; + } + } + + /** + * @private + */ + function parseStyleString(style, oStyle) { + var attr, value; + style.replace(/;$/, '').split(';').forEach(function (chunk) { + var pair = chunk.split(':'); + + attr = normalizeAttr(pair[0].trim().toLowerCase()); + value = normalizeValue(attr, pair[1].trim()); + + if (attr === 'font') { + parseFontDeclaration(value, oStyle); + } + else { + oStyle[attr] = value; + } + }); + } + + /** + * @private + */ + function parseStyleObject(style, oStyle) { + var attr, value; + for (var prop in style) { + if (typeof style[prop] === 'undefined') continue; + + attr = normalizeAttr(prop.toLowerCase()); + value = normalizeValue(attr, style[prop]); + + if (attr === 'font') { + parseFontDeclaration(value, oStyle); + } + else { + oStyle[attr] = value; + } + } + } + + /** + * @private + */ + function getGlobalStylesForElement(element) { + var nodeName = element.nodeName, + className = element.getAttribute('class'), + id = element.getAttribute('id'), + styles = { }; + + for (var rule in fabric.cssRules) { + var ruleMatchesElement = (className && new RegExp('^\\.' + className).test(rule)) || + (id && new RegExp('^#' + id).test(rule)) || + (new RegExp('^' + nodeName).test(rule)); + + if (ruleMatchesElement) { + for (var property in fabric.cssRules[rule]) { + styles[property] = fabric.cssRules[rule][property]; + } + } + } + + return styles; + } + + /** + * Parses an SVG document, converts it to an array of corresponding fabric.* instances and passes them to a callback + * @static + * @function + * @memberOf fabric + * @param {SVGDocument} doc SVG document to parse + * @param {Function} callback Callback to call when parsing is finished; It's being passed an array of elements (parsed from a document). + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + fabric.parseSVGDocument = (function() { + + var reAllowedSVGTagNames = /^(path|circle|polygon|polyline|ellipse|rect|line|image|text)$/; + + // http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute + // \d doesn't quite cut it (as we need to match an actual float number) + + // matches, e.g.: +14.56e-12, etc. + var reNum = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)'; + + var reViewBoxAttrValue = new RegExp( + '^' + + '\\s*(' + reNum + '+)\\s*,?' + + '\\s*(' + reNum + '+)\\s*,?' + + '\\s*(' + reNum + '+)\\s*,?' + + '\\s*(' + reNum + '+)\\s*' + + '$' + ); + + function hasAncestorWithNodeName(element, nodeName) { + while (element && (element = element.parentNode)) { + if (nodeName.test(element.nodeName)) { + return true; + } + } + return false; + } + + return function(doc, callback, reviver) { + if (!doc) return; + + var startTime = new Date(), + descendants = fabric.util.toArray(doc.getElementsByTagName('*')); + + if (descendants.length === 0) { + // we're likely in node, where "o3-xml" library fails to gEBTN("*") + // https://github.com/ajaxorg/node-o3-xml/issues/21 + descendants = doc.selectNodes("//*[name(.)!='svg']"); + var arr = [ ]; + for (var i = 0, len = descendants.length; i < len; i++) { + arr[i] = descendants[i]; + } + descendants = arr; + } + + var elements = descendants.filter(function(el) { + return reAllowedSVGTagNames.test(el.tagName) && + !hasAncestorWithNodeName(el, /^(?:pattern|defs)$/); // http://www.w3.org/TR/SVG/struct.html#DefsElement + }); + + if (!elements || (elements && !elements.length)) return; + + var viewBoxAttr = doc.getAttribute('viewBox'), + widthAttr = doc.getAttribute('width'), + heightAttr = doc.getAttribute('height'), + width = null, + height = null, + minX, + minY; + + if (viewBoxAttr && (viewBoxAttr = viewBoxAttr.match(reViewBoxAttrValue))) { + minX = parseInt(viewBoxAttr[1], 10); + minY = parseInt(viewBoxAttr[2], 10); + width = parseInt(viewBoxAttr[3], 10); + height = parseInt(viewBoxAttr[4], 10); + } + + // values of width/height attributes overwrite those extracted from viewbox attribute + width = widthAttr ? parseFloat(widthAttr) : width; + height = heightAttr ? parseFloat(heightAttr) : height; + + var options = { + width: width, + height: height + }; + + fabric.gradientDefs = fabric.getGradientDefs(doc); + fabric.cssRules = fabric.getCSSRules(doc); + + // Precedence of rules: style > class > attribute + + fabric.parseElements(elements, function(instances) { + fabric.documentParsingTime = new Date() - startTime; + if (callback) { + callback(instances, options); + } + }, clone(options), reviver); + }; + })(); + + /** + * Used for caching SVG documents (loaded via `fabric.Canvas#loadSVGFromURL`) + * @namespace + */ + var svgCache = { + + /** + * @param {String} name + * @param {Function} callback + */ + has: function (name, callback) { + callback(false); + }, + + /** + * @param {String} url + * @param {Function} callback + */ + get: function () { + /* NOOP */ + }, + + /** + * @param {String} url + * @param {Object} object + */ + set: function () { + /* NOOP */ + } + }; + + /** + * @private + */ + function _enlivenCachedObject(cachedObject) { + + var objects = cachedObject.objects, + options = cachedObject.options; + + objects = objects.map(function (o) { + return fabric[capitalize(o.type)].fromObject(o); + }); + + return ({ objects: objects, options: options }); + } + + /** + * @private + */ + function _createSVGPattern(markup, canvas, property) { + if (canvas[property] && canvas[property].toSVG) { + markup.push( + '', + '' + ); + } + } + + extend(fabric, { + + /** + * Initializes gradients on instances, according to gradients parsed from a document + * @param {Array} instances + */ + resolveGradients: function(instances) { + for (var i = instances.length; i--; ) { + var instanceFillValue = instances[i].get('fill'); + + if (!(/^url\(/).test(instanceFillValue)) continue; + + var gradientId = instanceFillValue.slice(5, instanceFillValue.length - 1); + + if (fabric.gradientDefs[gradientId]) { + instances[i].set('fill', + fabric.Gradient.fromElement(fabric.gradientDefs[gradientId], instances[i])); + } + } + }, + + /** + * 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 linearGradientEls = doc.getElementsByTagName('linearGradient'), + radialGradientEls = doc.getElementsByTagName('radialGradient'), + el, i, + gradientDefs = { }; + + i = linearGradientEls.length; + for (; i--; ) { + el = linearGradientEls[i]; + gradientDefs[el.getAttribute('id')] = el; + } + + i = radialGradientEls.length; + for (; i--; ) { + el = radialGradientEls[i]; + gradientDefs[el.getAttribute('id')] = el; + } + + return gradientDefs; + }, + + /** + * 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) { + + if (!element) { + return; + } + + var value, + parentAttributes = { }; + + // if there's a parent container (`g` node), parse its attributes recursively upwards + if (element.parentNode && /^g$/i.test(element.parentNode.nodeName)) { + parentAttributes = fabric.parseAttributes(element.parentNode, attributes); + } + + var ownAttributes = attributes.reduce(function(memo, attr) { + value = element.getAttribute(attr); + if (value) { + attr = normalizeAttr(attr); + value = normalizeValue(attr, value, parentAttributes); + + memo[attr] = value; + } + return memo; + }, { }); + + // add values parsed from style, which take precedence over attributes + // (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes) + ownAttributes = extend(ownAttributes, + extend(getGlobalStylesForElement(element), fabric.parseStyleAttribute(element))); + + return _setStrokeFillOpacity(extend(parentAttributes, ownAttributes)); + }, + + /** + * 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) { + fabric.ElementsParser.parse(elements, callback, options, reviver); + }, + + /** + * 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 points {String} points attribute string + * @return {Array} array of points + */ + parsePointsAttribute: function(points) { + + // points attribute is required and must not be empty + if (!points) return null; + + points = points.trim(); + var asPairs = points.indexOf(',') > -1; + + points = points.split(/\s+/); + var parsedPoints = [ ], i, len; + + // points could look like "10,20 30,40" or "10 20 30 40" + if (asPairs) { + i = 0; + len = points.length; + for (; i < len; i++) { + var pair = points[i].split(','); + parsedPoints.push({ x: parseFloat(pair[0]), y: parseFloat(pair[1]) }); + } + } + else { + i = 0; + len = points.length; + for (; i < len; i+=2) { + parsedPoints.push({ x: parseFloat(points[i]), y: parseFloat(points[i+1]) }); + } + } + + // odd number of points is an error + if (parsedPoints.length % 2 !== 0) { + // return null; + } + + return parsedPoints; + }, + + /** + * 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++) { + var styleContents = styles[0].textContent; + + // remove comments + styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, ''); + + rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g); + rules = rules.map(function(rule) { return rule.trim(); }); + + rules.forEach(function(rule) { + var match = rule.match(/([\s\S]*?)\s*\{([^}]*)\}/); + rule = match[1]; + var declaration = match[2].trim(), + propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/); + + if (!allRules[rule]) { + allRules[rule] = { }; + } + + for (var i = 0, len = propertyValuePairs.length; i < len; i++) { + var pair = propertyValuePairs[i].split(/\s*:\s*/), + property = pair[0], + value = pair[1]; + + allRules[rule][property] = value; + } + }); + } + + return allRules; + }, + + /** + * 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. + */ + loadSVGFromURL: function(url, callback, reviver) { + + url = url.replace(/^\n\s*/, '').trim(); + + svgCache.has(url, function (hasUrl) { + if (hasUrl) { + svgCache.get(url, function (value) { + var enlivedRecord = _enlivenCachedObject(value); + callback(enlivedRecord.objects, enlivedRecord.options); + }); + } + else { + new fabric.util.request(url, { + method: 'get', + onComplete: onComplete + }); + } + }); + + function onComplete(r) { + + var xml = r.responseXML; + if (xml && !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) return; + + fabric.parseSVGDocument(xml.documentElement, function (results, options) { + svgCache.set(url, { + objects: fabric.util.array.invoke(results, 'toObject'), + options: options + }); + callback(results, options); + }, reviver); + } + }, + + /** + * 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. + */ + loadSVGFromString: function(string, callback, reviver) { + string = string.trim(); + var doc; + if (typeof DOMParser !== 'undefined') { + var parser = new DOMParser(); + if (parser && parser.parseFromString) { + doc = parser.parseFromString(string, 'text/xml'); + } + } + else if (fabric.window.ActiveXObject) { + doc = new ActiveXObject('Microsoft.XMLDOM'); + doc.async = 'false'; + //IE chokes on DOCTYPE + doc.loadXML(string.replace(//i,'')); + } + + fabric.parseSVGDocument(doc.documentElement, function (results, options) { + callback(results, options); + }, reviver); + }, + + /** + * Creates markup containing SVG font faces + * @param {Array} objects Array of fabric objects + * @return {String} + */ + createSVGFontFacesMarkup: function(objects) { + var markup = ''; + + for (var i = 0, len = objects.length; i < len; i++) { + if (objects[i].type !== 'text' || !objects[i].path) continue; + + markup += [ + '@font-face {', + 'font-family: ', objects[i].fontFamily, '; ', + 'src: url(\'', objects[i].path, '\')', + '}' + ].join(''); + } + + if (markup) { + markup = [ + '' + ].join(''); + } + + return markup; + }, + + /** + * Creates markup containing SVG referenced elements like patterns, gradients etc. + * @param {fabric.Canvas} canvas instance of fabric.Canvas + * @return {String} + */ + createSVGRefElementsMarkup: function(canvas) { + var markup = [ ]; + + _createSVGPattern(markup, canvas, 'backgroundColor'); + _createSVGPattern(markup, canvas, 'overlayColor'); + + return markup.join(''); + } + }); + +})(typeof exports !== 'undefined' ? exports : this); + + +fabric.ElementsParser = { + + parse: function(elements, callback, options, reviver) { + + this.elements = elements; + this.callback = callback; + this.options = options; + this.reviver = reviver; + + this.instances = new Array(elements.length); + this.numElements = elements.length; + + this.createObjects(); + }, + + createObjects: function() { + for (var i = 0, len = this.elements.length; i < len; i++) { + (function(_this, i) { + setTimeout(function() { + _this.createObject(_this.elements[i], i); + }, 0); + })(this, i); + } + }, + + createObject: function(el, index) { + var klass = fabric[fabric.util.string.capitalize(el.tagName)]; + if (klass && klass.fromElement) { + try { + this._createObject(klass, el, index); + } + catch(err) { + fabric.log(err); + } + } + else { + this.checkIfDone(); + } + }, + + _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.reviver && this.reviver(el, obj); + this.instances.splice(index, 0, obj); + this.checkIfDone(); + } + }, + + createCallback: function(index, el) { + var _this = this; + return function(obj) { + _this.reviver && _this.reviver(el, obj); + _this.instances.splice(index, 0, obj); + _this.checkIfDone(); + }; + }, + + checkIfDone: function() { + if (--this.numElements === 0) { + this.instances = this.instances.filter(function(el) { + return el != null; + }); + fabric.resolveGradients(this.instances); + 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 */ { + + constructor: Point, + + /** + * Adds another point to this one and returns another one + * @param {fabric.Point} that + * @return {fabric.Point} new Point instance with added values + */ + add: function (that) { + return new Point(this.x + that.x, this.y + that.y); + }, + + /** + * Adds another point to this one + * @param {fabric.Point} that + * @return {fabric.Point} thisArg + */ + addEquals: function (that) { + this.x += that.x; + this.y += that.y; + return this; + }, + + /** + * Adds value to this point and returns a new one + * @param {Number} scalar + * @return {fabric.Point} new Point with added value + */ + scalarAdd: function (scalar) { + return new Point(this.x + scalar, this.y + scalar); + }, + + /** + * Adds value to this point + * @param {Number} scalar + * @param {fabric.Point} thisArg + */ + scalarAddEquals: function (scalar) { + this.x += scalar; + this.y += scalar; + return this; + }, + + /** + * Subtracts another point from this point and returns a new one + * @param {fabric.Point} that + * @return {fabric.Point} new Point object with subtracted values + */ + subtract: function (that) { + return new Point(this.x - that.x, this.y - that.y); + }, + + /** + * Subtracts another point from this point + * @param {fabric.Point} that + * @return {fabric.Point} thisArg + */ + subtractEquals: function (that) { + this.x -= that.x; + this.y -= that.y; + return this; + }, + + /** + * Subtracts value from this point and returns a new one + * @param {Number} scalar + * @return {fabric.Point} + */ + scalarSubtract: function (scalar) { + return new Point(this.x - scalar, this.y - scalar); + }, + + /** + * Subtracts value from this point + * @param {Number} scalar + * @return {fabric.Point} thisArg + */ + scalarSubtractEquals: function (scalar) { + this.x -= scalar; + this.y -= scalar; + return this; + }, + + /** + * Miltiplies this point by a value and returns a new one + * @param {Number} scalar + * @return {fabric.Point} + */ + multiply: function (scalar) { + return new Point(this.x * scalar, this.y * scalar); + }, + + /** + * Miltiplies this point by a value + * @param {Number} scalar + * @return {fabric.Point} thisArg + */ + multiplyEquals: function (scalar) { + this.x *= scalar; + this.y *= scalar; + return this; + }, + + /** + * Divides this point by a value and returns a new one + * @param {Number} scalar + * @return {fabric.Point} + */ + divide: function (scalar) { + return new Point(this.x / scalar, this.y / scalar); + }, + + /** + * Divides this point by a value + * @param {Number} scalar + * @return {fabric.Point} thisArg + */ + divideEquals: function (scalar) { + this.x /= scalar; + this.y /= scalar; + return this; + }, + + /** + * Returns true if this point is equal to another one + * @param {fabric.Point} that + * @return {Boolean} + */ + eq: function (that) { + return (this.x === that.x && this.y === that.y); + }, + + /** + * Returns true if this point is less than another one + * @param {fabric.Point} that + * @return {Boolean} + */ + lt: function (that) { + return (this.x < that.x && this.y < that.y); + }, + + /** + * Returns true if this point is less than or equal to another one + * @param {fabric.Point} that + * @return {Boolean} + */ + lte: function (that) { + return (this.x <= that.x && this.y <= that.y); + }, + + /** + + * Returns true if this point is greater another one + * @param {fabric.Point} that + * @return {Boolean} + */ + gt: function (that) { + return (this.x > that.x && this.y > that.y); + }, + + /** + * Returns true if this point is greater than or equal to another one + * @param {fabric.Point} that + * @return {Boolean} + */ + gte: function (that) { + return (this.x >= that.x && this.y >= that.y); + }, + + /** + * Returns new point which is the result of linear interpolation with this one and another one + * @param {fabric.Point} that + * @param {Number} t + * @return {fabric.Point} + */ + lerp: function (that, t) { + return new Point(this.x + (that.x - this.x) * t, this.y + (that.y - this.y) * t); + }, + + /** + * Returns distance from this point and another one + * @param {fabric.Point} that + * @return {Number} + */ + distanceFrom: function (that) { + var dx = this.x - that.x, + dy = this.y - that.y; + return Math.sqrt(dx * dx + dy * dy); + }, + + /** + * Returns the point between this point and another one + * @param {fabric.Point} that + * @return {fabric.Point} + */ + midPointFrom: function (that) { + return new Point(this.x + (that.x - this.x)/2, this.y + (that.y - this.y)/2); + }, + + /** + * Returns a new point which is the min of this and another one + * @param {fabric.Point} that + * @return {fabric.Point} + */ + min: function (that) { + return new Point(Math.min(this.x, that.x), Math.min(this.y, that.y)); + }, + + /** + * Returns a new point which is the max of this and another one + * @param {fabric.Point} that + * @return {fabric.Point} + */ + max: function (that) { + return new Point(Math.max(this.x, that.x), Math.max(this.y, that.y)); + }, + + /** + * Returns string representation of this point + * @return {String} + */ + toString: function () { + return this.x + "," + this.y; + }, + + /** + * Sets x/y of this point + * @param {Number} x + * @return {Number} y + */ + setXY: function (x, y) { + this.x = x; + this.y = y; + }, + + /** + * Sets x/y of this point from another point + * @param {fabric.Point} that + */ + setFromPoint: function (that) { + this.x = that.x; + this.y = that.y; + }, + + /** + * Swaps x/y of this point and another point + * @param {fabric.Point} that + */ + swap: function (that) { + var x = this.x, + y = this.y; + this.x = that.x; + this.y = that.y; + that.x = x; + that.y = y; + } + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + "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 */ { + + /** + * Appends a point to intersection + * @param {fabric.Point} point + */ + appendPoint: function (point) { + this.points.push(point); + }, + + /** + * Appends points to intersection + * @param {Array} points + */ + appendPoints: function (points) { + this.points = this.points.concat(points); + } + }; + + /** + * Checks if one line intersects another + * @static + * @param {fabric.Point} a1 + * @param {fabric.Point} a2 + * @param {fabric.Point} b1 + * @param {fabric.Point} b2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectLineLine = function (a1, a2, b1, b2) { + var result, + ua_t = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x), + ub_t = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x), + u_b = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y); + if (u_b !== 0) { + var ua = ua_t / u_b, + ub = ub_t / u_b; + if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) { + result = new Intersection("Intersection"); + result.points.push(new fabric.Point(a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y))); + } + else { + result = new Intersection(); + } + } + else { + if (ua_t === 0 || ub_t === 0) { + result = new Intersection("Coincident"); + } + else { + result = new Intersection("Parallel"); + } + } + return result; + }; + + /** + * Checks if line intersects polygon + * @static + * @param {fabric.Point} a1 + * @param {fabric.Point} a2 + * @param {Array} points + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectLinePolygon = function(a1,a2,points){ + var result = new Intersection(), + length = points.length; + + for (var i = 0; i < length; i++) { + var b1 = points[i], + b2 = points[(i+1) % length], + inter = Intersection.intersectLineLine(a1, a2, b1, b2); + + result.appendPoints(inter.points); + } + if (result.points.length > 0) { + result.status = "Intersection"; + } + return result; + }; + + /** + * Checks if polygon intersects another polygon + * @static + * @param {Array} points1 + * @param {Array} points2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectPolygonPolygon = function (points1, points2) { + var result = new Intersection(), + length = points1.length; + + for (var i = 0; i < length; i++) { + var a1 = points1[i], + a2 = points1[(i+1) % length], + inter = Intersection.intersectLinePolygon(a1, a2, points2); + + result.appendPoints(inter.points); + } + if (result.points.length > 0) { + result.status = "Intersection"; + } + return result; + }; + + /** + * Checks if polygon intersects rectangle + * @static + * @param {Array} points + * @param {Number} r1 + * @param {Number} r2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectPolygonRectangle = function (points, r1, r2) { + var min = r1.min(r2), + max = r1.max(r2), + topRight = new fabric.Point(max.x, min.y), + bottomLeft = new fabric.Point(min.x, max.y), + inter1 = Intersection.intersectLinePolygon(min, topRight, points), + inter2 = Intersection.intersectLinePolygon(topRight, max, points), + inter3 = Intersection.intersectLinePolygon(max, bottomLeft, points), + inter4 = Intersection.intersectLinePolygon(bottomLeft, min, points), + result = new Intersection(); + + result.appendPoints(inter1.points); + result.appendPoints(inter2.points); + result.appendPoints(inter3.points); + result.appendPoints(inter4.points); + + if (result.points.length > 0) { + result.status = "Intersection"; + } + return result; + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + "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) format + * @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]; + } + + source = Color.sourceFromHex(color); + + if (!source) { + source = Color.sourceFromRgb(color); + } + if (!source) { + source = Color.sourceFromHsl(color); + } + if (source) { + this.setSource(source); + } + }, + + /** + * Adapted from https://github.com/mjijackson + * @private + * @param {Number} r Red color value + * @param {Number} g Green color value + * @param {Number} b Blue color value + * @return {Array} Hsl color + */ + _rgbToHsl: function(r, g, b) { + r /= 255, g /= 255, b /= 255; + + var h, s, l, + max = fabric.util.array.max([r, g, b]), + min = fabric.util.array.min([r, g, b]); + + l = (max + min) / 2; + + if (max === min) { + h = s = 0; // achromatic + } + else { + var d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + } + h /= 6; + } + + return [ + Math.round(h * 360), + Math.round(s * 100), + Math.round(l * 100) + ]; + }, + + /** + * Returns source of this color (where source is an array representation; ex: [200, 200, 100, 1]) + * @return {Array} + */ + getSource: function() { + return this._source; + }, + + /** + * Sets source of this color (where source is an array representation; ex: [200, 200, 100, 1]) + * @param {Array} source + */ + setSource: function(source) { + this._source = source; + }, + + /** + * Returns color represenation in RGB format + * @return {String} ex: rgb(0-255,0-255,0-255) + */ + toRgb: function() { + var source = this.getSource(); + return 'rgb(' + source[0] + ',' + source[1] + ',' + source[2] + ')'; + }, + + /** + * Returns color represenation in RGBA format + * @return {String} ex: rgba(0-255,0-255,0-255,0-1) + */ + toRgba: function() { + var source = this.getSource(); + return 'rgba(' + source[0] + ',' + source[1] + ',' + source[2] + ',' + source[3] + ')'; + }, + + /** + * Returns color represenation in HSL format + * @return {String} ex: hsl(0-360,0%-100%,0%-100%) + */ + toHsl: function() { + var source = this.getSource(), + hsl = this._rgbToHsl(source[0], source[1], source[2]); + + return 'hsl(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%)'; + }, + + /** + * Returns color represenation in HSLA format + * @return {String} ex: hsla(0-360,0%-100%,0%-100%,0-1) + */ + toHsla: function() { + var source = this.getSource(), + hsl = this._rgbToHsl(source[0], source[1], source[2]); + + return 'hsla(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%,' + source[3] + ')'; + }, + + /** + * Returns color represenation in HEX format + * @return {String} ex: FF5555 + */ + toHex: function() { + var source = this.getSource(); + + var r = source[0].toString(16); + r = (r.length === 1) ? ('0' + r) : r; + + var g = source[1].toString(16); + g = (g.length === 1) ? ('0' + g) : g; + + var b = source[2].toString(16); + b = (b.length === 1) ? ('0' + b) : b; + + return r.toUpperCase() + g.toUpperCase() + b.toUpperCase(); + }, + + /** + * Gets value of alpha channel for this color + * @return {Number} 0-1 + */ + getAlpha: function() { + return this.getSource()[3]; + }, + + /** + * Sets value of alpha channel for this color + * @param {Number} alpha Alpha value 0-1 + * @return {fabric.Color} thisArg + */ + setAlpha: function(alpha) { + var source = this.getSource(); + source[3] = alpha; + this.setSource(source); + return this; + }, + + /** + * Transforms color to its grayscale representation + * @return {fabric.Color} thisArg + */ + toGrayscale: function() { + var source = this.getSource(), + average = parseInt((source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), 10), + currentAlpha = source[3]; + this.setSource([average, average, average, currentAlpha]); + return this; + }, + + /** + * Transforms color to its black and white representation + * @param {Number} threshold + * @return {fabric.Color} thisArg + */ + toBlackWhite: function(threshold) { + var source = this.getSource(), + average = (source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), + currentAlpha = source[3]; + + threshold = threshold || 127; + + average = (Number(average) < Number(threshold)) ? 0 : 255; + this.setSource([average, average, average, currentAlpha]); + return this; + }, + + /** + * Overlays color with another color + * @param {String|fabric.Color} otherColor + * @return {fabric.Color} thisArg + */ + overlayWith: function(otherColor) { + if (!(otherColor instanceof Color)) { + otherColor = new Color(otherColor); + } + + var result = [], + alpha = this.getAlpha(), + otherAlpha = 0.5, + source = this.getSource(), + otherSource = otherColor.getSource(); + + for (var i = 0; i < 3; i++) { + result.push(Math.round((source[i] * (1 - otherAlpha)) + (otherSource[i] * otherAlpha))); + } + + result[3] = alpha; + this.setSource(result); + return this; + } + }; + + /** + * Regex matching color in RGB or RGBA formats (ex: rgb(0, 0, 0), rgba(255, 100, 10, 0.5), rgba( 255 , 100 , 10 , 0.5 ), rgb(1,1,1), rgba(100%, 60%, 10%, 0.5)) + * @static + * @field + * @memberOf fabric.Color + */ + fabric.Color.reRGBa = /^rgba?\(\s*(\d{1,3}\%?)\s*,\s*(\d{1,3}\%?)\s*,\s*(\d{1,3}\%?)\s*(?:\s*,\s*(\d+(?:\.\d+)?)\s*)?\)$/; + + /** + * Regex matching color in HSL or HSLA formats (ex: hsl(200, 80%, 10%), hsla(300, 50%, 80%, 0.5), hsla( 300 , 50% , 80% , 0.5 )) + * @static + * @field + * @memberOf fabric.Color + */ + fabric.Color.reHSLa = /^hsla?\(\s*(\d{1,3})\s*,\s*(\d{1,3}\%)\s*,\s*(\d{1,3}\%)\s*(?:\s*,\s*(\d+(?:\.\d+)?)\s*)?\)$/; + + /** + * Regex matching color in HEX format (ex: #FF5555, 010155, aff) + * @static + * @field + * @memberOf fabric.Color + */ + fabric.Color.reHex = /^#?([0-9a-f]{6}|[0-9a-f]{3})$/i; + + /** + * Map of the 17 basic color names with HEX code + * @static + * @field + * @memberOf fabric.Color + * @see: http://www.w3.org/TR/CSS2/syndata.html#color-units + */ + fabric.Color.colorNameMap = { + 'aqua': '#00FFFF', + 'black': '#000000', + 'blue': '#0000FF', + 'fuchsia': '#FF00FF', + 'gray': '#808080', + 'green': '#008000', + 'lime': '#00FF00', + 'maroon': '#800000', + 'navy': '#000080', + 'olive': '#808000', + 'orange': '#FFA500', + 'purple': '#800080', + 'red': '#FF0000', + 'silver': '#C0C0C0', + 'teal': '#008080', + 'white': '#FFFFFF', + 'yellow': '#FFFF00' + }; + + /** + * @private + * @param {Number} p + * @param {Number} q + * @param {Number} t + * @return {Number} + */ + function hue2rgb(p, q, t){ + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1/6) return p + (q - p) * 6 * t; + if (t < 1/2) return q; + if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; + return p; + } + + /** + * Returns new color object, when given a color in RGB format + * @memberOf fabric.Color + * @param {String} color Color value ex: rgb(0-255,0-255,0-255) + * @return {fabric.Color} + */ + fabric.Color.fromRgb = function(color) { + return Color.fromSource(Color.sourceFromRgb(color)); + }; + + /** + * Returns array represenatation (ex: [100, 100, 200, 1]) of a color that's in RGB or RGBA format + * @memberOf fabric.Color + * @param {String} color Color value ex: rgb(0-255,0-255,0-255), rgb(0%-100%,0%-100%,0%-100%) + * @return {Array} source + */ + fabric.Color.sourceFromRgb = function(color) { + var match = color.match(Color.reRGBa); + if (match) { + var r = parseInt(match[1], 10) / (/%$/.test(match[1]) ? 100 : 1) * (/%$/.test(match[1]) ? 255 : 1), + g = parseInt(match[2], 10) / (/%$/.test(match[2]) ? 100 : 1) * (/%$/.test(match[2]) ? 255 : 1), + b = parseInt(match[3], 10) / (/%$/.test(match[3]) ? 100 : 1) * (/%$/.test(match[3]) ? 255 : 1); + + return [ + parseInt(r, 10), + parseInt(g, 10), + parseInt(b, 10), + match[4] ? parseFloat(match[4]) : 1 + ]; + } + }; + + /** + * Returns new color object, when given a color in RGBA format + * @static + * @function + * @memberOf fabric.Color + * @param {String} color + * @return {fabric.Color} + */ + fabric.Color.fromRgba = Color.fromRgb; + + /** + * Returns new color object, when given a color in HSL format + * @param {String} color Color value ex: hsl(0-260,0%-100%,0%-100%) + * @memberOf fabric.Color + * @return {fabric.Color} + */ + fabric.Color.fromHsl = function(color) { + return Color.fromSource(Color.sourceFromHsl(color)); + }; + + /** + * Returns array represenatation (ex: [100, 100, 200, 1]) of a color that's in HSL or HSLA format. + * Adapted from https://github.com/mjijackson + * @memberOf fabric.Color + * @param {String} color Color value ex: hsl(0-360,0%-100%,0%-100%) or hsla(0-360,0%-100%,0%-100%, 0-1) + * @return {Array} source + * @see http://http://www.w3.org/TR/css3-color/#hsl-color + */ + fabric.Color.sourceFromHsl = function(color) { + var match = color.match(Color.reHSLa); + if (!match) return; + + var h = (((parseFloat(match[1]) % 360) + 360) % 360) / 360, + s = parseFloat(match[2]) / (/%$/.test(match[2]) ? 100 : 1), + l = parseFloat(match[3]) / (/%$/.test(match[3]) ? 100 : 1), + r, g, b; + + if (s === 0) { + r = g = b = l; + } + else { + var q = l <= 0.5 ? l * (s + 1) : l + s - l * s; + var p = l * 2 - q; + + r = hue2rgb(p, q, h + 1/3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1/3); + } + + return [ + Math.round(r * 255), + Math.round(g * 255), + Math.round(b * 255), + match[4] ? parseFloat(match[4]) : 1 + ]; + }; + + /** + * Returns new color object, when given a color in HSLA format + * @static + * @function + * @memberOf fabric.Color + * @param {String} color + * @return {fabric.Color} + */ + fabric.Color.fromHsla = Color.fromHsl; + + /** + * Returns new color object, when given a color in HEX format + * @static + * @memberOf fabric.Color + * @param {String} color Color value ex: FF5555 + * @return {fabric.Color} + */ + fabric.Color.fromHex = function(color) { + return Color.fromSource(Color.sourceFromHex(color)); + }; + + /** + * Returns array represenatation (ex: [100, 100, 200, 1]) of a color that's in HEX format + * @static + * @memberOf fabric.Color + * @param {String} color ex: FF5555 + * @return {Array} source + */ + fabric.Color.sourceFromHex = function(color) { + if (color.match(Color.reHex)) { + var value = color.slice(color.indexOf('#') + 1), + isShortNotation = (value.length === 3), + r = isShortNotation ? (value.charAt(0) + value.charAt(0)) : value.substring(0, 2), + g = isShortNotation ? (value.charAt(1) + value.charAt(1)) : value.substring(2, 4), + b = isShortNotation ? (value.charAt(2) + value.charAt(2)) : value.substring(4, 6); + + return [ + parseInt(r, 16), + parseInt(g, 16), + parseInt(b, 16), + 1 + ]; + } + }; + + /** + * Returns new color object, when given color in array representation (ex: [200, 100, 100, 0.5]) + * @static + * @memberOf fabric.Color + * @param {Array} source + * @return {fabric.Color} + */ + fabric.Color.fromSource = function(source) { + var oColor = new Color(); + oColor.setSource(source); + return oColor; + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + + /* _FROM_SVG_START_ */ + function getColorStop(el) { + var style = el.getAttribute('style'), + offset = el.getAttribute('offset'), + color, opacity; + + // convert percents to absolute values + offset = parseFloat(offset) / (/%$/.test(offset) ? 100 : 1); + + if (style) { + var keyValuePairs = style.split(/\s*;\s*/); + + if (keyValuePairs[keyValuePairs.length-1] === '') { + keyValuePairs.pop(); + } + + for (var i = keyValuePairs.length; i--; ) { + + var split = keyValuePairs[i].split(/\s*:\s*/), + key = split[0].trim(), + value = split[1].trim(); + + if (key === 'stop-color') { + color = value; + } + else if (key === 'stop-opacity') { + opacity = value; + } + } + } + + if (!color) { + color = el.getAttribute('stop-color') || 'rgb(0,0,0)'; + } + if (!opacity) { + opacity = el.getAttribute('stop-opacity'); + } + + // convert rgba color to rgb color - alpha value has no affect in svg + color = new fabric.Color(color).toRgb(); + + return { + offset: offset, + color: color, + opacity: isNaN(parseFloat(opacity)) ? 1 : parseFloat(opacity) + }; + } + + 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_ */ + + /** + * 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 */ { + + /** + * Constructor + * @param {Object} [options] Options object with type, coords, gradientUnits and colorStops + * @return {fabric.Gradient} thisArg + */ + initialize: function(options) { + options || (options = { }); + + var coords = { }; + + this.id = fabric.Object.__uid++; + this.type = options.type || 'linear'; + + coords = { + x1: options.coords.x1 || 0, + y1: options.coords.y1 || 0, + x2: options.coords.x2 || 0, + y2: options.coords.y2 || 0 + }; + + if (this.type === 'radial') { + coords.r1 = options.coords.r1 || 0; + coords.r2 = options.coords.r2 || 0; + } + + this.coords = coords; + this.gradientUnits = options.gradientUnits || 'objectBoundingBox'; + this.colorStops = options.colorStops.slice(); + }, + + /** + * Adds another colorStop + * @param {Object} colorStop Object with offset and color + * @return {fabric.Gradient} thisArg + */ + addColorStop: function(colorStop) { + for (var position in colorStop) { + var color = new fabric.Color(colorStop[position]); + this.colorStops.push({offset: position, color: color.toRgb(), opacity: color.getAlpha()}); + } + return this; + }, + + /** + * Returns object representation of a gradient + * @return {Object} + */ + toObject: function() { + return { + type: this.type, + coords: this.coords, + gradientUnits: this.gradientUnits, + colorStops: this.colorStops + }; + }, + + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of an gradient + * @param {Object} object Object to create a gradient for + * @param {Boolean} normalize Whether coords should be normalized + * @return {String} SVG representation of an gradient (linear/radial) + */ + toSVG: function(object, normalize) { + var coords = fabric.util.object.clone(this.coords), + markup; + + // colorStops must be sorted ascending + this.colorStops.sort(function(a, b) { + return a.offset - b.offset; + }); + + if (normalize && this.gradientUnits === 'userSpaceOnUse') { + coords.x1 += object.width / 2; + coords.y1 += object.height / 2; + coords.x2 += object.width / 2; + coords.y2 += object.height / 2; + } + else if (this.gradientUnits === 'objectBoundingBox') { + _convertValuesToPercentUnits(object, coords); + } + + if (this.type === 'linear') { + markup = [ + '' + ]; + } + else if (this.type === 'radial') { + markup = [ + '' + ]; + } + + for (var i = 0; i < this.colorStops.length; i++) { + markup.push( + '' + ); + } + + markup.push((this.type === 'linear' ? '' : '')); + + return markup.join(''); + }, + /* _TO_SVG_END_ */ + + /** + * Returns an instance of CanvasGradient + * @param {CanvasRenderingContext2D} ctx Context to render on + * @return {CanvasGradient} + */ + toLive: function(ctx) { + var gradient; + + if (!this.type) return; + + if (this.type === 'linear') { + gradient = ctx.createLinearGradient( + this.coords.x1, this.coords.y1, this.coords.x2, this.coords.y2); + } + else if (this.type === 'radial') { + gradient = ctx.createRadialGradient( + this.coords.x1, this.coords.y1, this.coords.r1, this.coords.x2, this.coords.y2, this.coords.r2); + } + + for (var i = 0, len = this.colorStops.length; i < len; i++) { + var color = this.colorStops[i].color, + opacity = this.colorStops[i].opacity, + offset = this.colorStops[i].offset; + + if (typeof opacity !== 'undefined') { + color = new fabric.Color(color).setAlpha(opacity).toRgba(); + } + gradient.addColorStop(parseFloat(offset), color); + } + + return gradient; + } + }); + + fabric.util.object.extend(fabric.Gradient, { + + /* _FROM_SVG_START_ */ + /** + * Returns {@link fabric.Gradient} instance from an SVG element + * @static + * @memberof fabric.Gradient + * @param {SVGGradientElement} el SVG gradient element + * @param {fabric.Object} instance + * @return {fabric.Gradient} Gradient instance + * @see http://www.w3.org/TR/SVG/pservers.html#LinearGradientElement + * @see http://www.w3.org/TR/SVG/pservers.html#RadialGradientElement + */ + fromElement: function(el, instance) { + + /** + * @example: + * + * + * + * + * + * + * OR + * + * + * + * + * + * + * OR + * + * + * + * + * + * + * + * OR + * + * + * + * + * + * + * + */ + + var colorStopEls = el.getElementsByTagName('stop'), + type = (el.nodeName === 'linearGradient' ? 'linear' : 'radial'), + gradientUnits = el.getAttribute('gradientUnits') || 'objectBoundingBox', + colorStops = [], + coords = { }; + + if (type === 'linear') { + coords = getLinearCoords(el); + } + else if (type === 'radial') { + coords = getRadialCoords(el); + } + + for (var i = colorStopEls.length; i--; ) { + colorStops.push(getColorStop(colorStopEls[i])); + } + + _convertPercentUnitsToValues(instance, coords); + + return new fabric.Gradient({ + type: type, + coords: coords, + gradientUnits: gradientUnits, + colorStops: colorStops + }); + }, + /* _FROM_SVG_END_ */ + + /** + * Returns {@link fabric.Gradient} instance from its object representation + * @static + * @memberof fabric.Gradient + * @param {Object} obj + * @param {Object} [options] Options object + */ + forObject: function(obj, options) { + options || (options = { }); + _convertPercentUnitsToValues(obj, options); + return new fabric.Gradient(options); + } + }); + + /** + * @private + */ + function _convertPercentUnitsToValues(object, options) { + for (var prop in options) { + if (typeof options[prop] === 'string' && /^\d+%$/.test(options[prop])) { + var percents = parseFloat(options[prop], 10); + if (prop === 'x1' || prop === 'x2' || prop === 'r2') { + options[prop] = fabric.util.toFixed(object.width * percents / 100, 2); + } + else if (prop === 'y1' || prop === 'y2') { + options[prop] = fabric.util.toFixed(object.height * percents / 100, 2); + } + } + normalize(options, prop, object); + } + } + + // normalize rendering point (should be from top/left corner rather than center of the shape) + function normalize(options, prop, object) { + if (prop === 'x1' || prop === 'x2') { + options[prop] -= fabric.util.toFixed(object.width / 2, 2); + } + else if (prop === 'y1' || prop === 'y2') { + options[prop] -= fabric.util.toFixed(object.height / 2, 2); + } + } + + /* _TO_SVG_START_ */ + /** + * @private + */ + function _convertValuesToPercentUnits(object, options) { + for (var prop in options) { + + normalize(options, prop, object); + + // convert to percent units + if (prop === 'x1' || prop === 'x2' || prop === 'r2') { + options[prop] = fabric.util.toFixed(options[prop] / object.width * 100, 2) + '%'; + } + else if (prop === 'y1' || prop === 'y2') { + options[prop] = fabric.util.toFixed(options[prop] / object.height * 100, 2) + '%'; + } + } + } + /* _TO_SVG_END_ */ + +})(); + + +/** + * 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 + * @return {fabric.Pattern} thisArg + */ + initialize: function(options) { + options || (options = { }); + + this.id = fabric.Object.__uid++; + + if (options.source) { + if (typeof options.source === 'string') { + // function string + if (typeof fabric.util.getFunctionBody(options.source) !== 'undefined') { + this.source = new Function(fabric.util.getFunctionBody(options.source)); + } + else { + // img src string + var _this = this; + this.source = fabric.util.createImage(); + fabric.util.loadImage(options.source, function(img) { + _this.source = img; + }); + } + } + else { + // img element + this.source = options.source; + } + } + if (options.repeat) { + this.repeat = options.repeat; + } + if (options.offsetX) { + this.offsetX = options.offsetX; + } + if (options.offsetY) { + this.offsetY = options.offsetY; + } + }, + + /** + * Returns object representation of a pattern + * @return {Object} Object representation of a pattern instance + */ + toObject: function() { + + var source; + + // callback + if (typeof this.source === 'function') { + source = String(this.source); + } + // element + else if (typeof this.source.src === 'string') { + source = this.source.src; + } + + return { + source: source, + repeat: this.repeat, + offsetX: this.offsetX, + offsetY: this.offsetY + }; + }, + + /* _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; + var patternWidth = patternSource.width / object.getWidth(); + var patternHeight = patternSource.height / object.getHeight(); + var patternImgSrc = ''; + + if (patternSource.src) { + patternImgSrc = patternSource.src; + } + else if (patternSource.toDataURL) { + patternImgSrc = patternSource.toDataURL(); + } + + return '' + + '' + + ''; + }, + /* _TO_SVG_END_ */ + + /** + * 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 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 = { }); + + 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(); + + var offsetsAndBlur = fabric.Shadow.reOffsetsAndBlur.exec(shadowStr) || [ ], + color = shadowStr.replace(fabric.Shadow.reOffsetsAndBlur, '') || 'rgb(0,0,0)'; + + return { + color: color.trim(), + offsetX: parseInt(offsetsAndBlur[1], 10) || 0, + offsetY: parseInt(offsetsAndBlur[2], 10) || 0, + blur: parseInt(offsetsAndBlur[3], 10) || 0 + }; + }, + + /** + * Returns a string representation of an instance + * @see http://www.w3.org/TR/css-text-decor-3/#text-shadow + * @return {String} Returns CSS3 text-shadow declaration + */ + toString: function() { + return [this.offsetX, this.offsetY, this.blur, this.color].join('px '); + }, + + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of a shadow + * @param {fabric.Object} object + * @return {String} SVG representation of a shadow + */ + toSVG: function(object) { + var mode = 'SourceAlpha'; + + if (object && (object.fill === this.color || object.stroke === this.color)) { + mode = 'SourceGraphic'; + } + + return ( + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''); + }, + /* _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 + }; + } + var obj = { }, proto = fabric.Shadow.prototype; + if (this.color !== proto.color) { + obj.color = this.color; + } + if (this.blur !== proto.blur) { + obj.blur = this.blur; + } + if (this.offsetX !== proto.offsetX) { + obj.offsetX = this.offsetX; + } + if (this.offsetY !== proto.offsetY) { + obj.offsetY = this.offsetY; + } + 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 + */ + 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, + + 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(/** @lends fabric.StaticCanvas.prototype */ { + + /** + * Constructor + * @param {HTMLElement | String} el <canvas> element to initialize instance on + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(el, options) { + options || (options = { }); + + this._initStatic(el, options); + fabric.StaticCanvas.activeInstance = this; + }, + + /** + * Background color of canvas instance. + * 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: true, + + /** + * Indicates whether {@link fabric.Collection.add}, {@link fabric.Collection.insertAt} and {@link fabric.Collection.remove} should also re-render canvas. + * Disabling this option could give a great performance boost when adding/removing a lot of objects to/from canvas at once + * (followed by a manual rendering after addition/deletion) + * @type Boolean + * @default + */ + renderOnAddRemove: true, + + /** + * Function that determines clipping of entire canvas area + * Being passed context as first argument. See clipping canvas area in {@link https://github.com/kangax/fabric.js/wiki/FAQ} + * @type Function + * @default + */ + clipTo: null, + + /** + * Indicates whether object controls (borders/controls) are rendered above overlay image + * @type Boolean + * @default + */ + controlsAboveOverlay: false, + + /** + * Indicates whether the browser can be scrolled when using a touchscreen and dragging on the canvas + * @type Boolean + * @default + */ + allowTouchScrolling: false, + + /** + * Callback; invoked right before object is about to be scaled/rotated + * @param {fabric.Object} target Object that's about to be scaled/rotated + */ + onBeforeScaleRotate: function () { + /* NOOP */ + }, + + /** + * @private + * @param {HTMLElement | String} el <canvas> element to initialize instance on + * @param {Object} [options] Options object + */ + _initStatic: function(el, options) { + this._objects = []; + + this._createLowerCanvas(el); + this._initOptions(options); + + if (options.overlayImage) { + this.setOverlayImage(options.overlayImage, this.renderAll.bind(this)); + } + if (options.backgroundImage) { + this.setBackgroundImage(options.backgroundImage, this.renderAll.bind(this)); + } + if (options.backgroundColor) { + this.setBackgroundColor(options.backgroundColor, this.renderAll.bind(this)); + } + if (options.overlayColor) { + this.setOverlayColor(options.overlayColor, this.renderAll.bind(this)); + } + this.calcOffset(); + }, + + /** + * Calculates canvas element offset relative to the document + * This method is also attached as "resize" event handler of window + * @return {fabric.Canvas} instance + * @chainable + */ + calcOffset: function () { + this._offset = getElementOffset(this.lowerCanvasEl); + return this; + }, + + /** + * Sets {@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' + * }); + */ + 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' + * }); + */ + 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 + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundImage|backgroundImage} + * or {@link fabric.StaticCanvas#overlayImage|overlayImage}) + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image 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) { + this[property] = new fabric.Image(img, options); + callback && callback(); + }, this); + } + else { + this[property] = image; + callback && callback(); + } + + return this; + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundColor|backgroundColor} + * or {@link fabric.StaticCanvas#overlayColor|overlayColor}) + * @param {(Object|String)} color Object with pattern information or color value + * @param {Function} [callback] Callback is invoked when color is set + */ + __setBgOverlayColor: function(property, color, callback) { + if (color.source) { + var _this = this; + fabric.util.loadImage(color.source, function(img) { + _this[property] = new fabric.Pattern({ + source: img, + repeat: color.repeat, + offsetX: color.offsetX, + offsetY: color.offsetY + }); + callback && callback(); + }); + } + else { + this[property] = color; + callback && callback(); + } + + return this; + }, + + /** + * @private + */ + _createCanvasElement: function() { + var element = fabric.document.createElement('canvas'); + if (!element.style) { + element.style = { }; + } + if (!element) { + throw CANVAS_INIT_ERROR; + } + this._initCanvasElement(element); + return element; + }, + + /** + * @private + * @param {HTMLElement} element + */ + _initCanvasElement: function(element) { + fabric.util.createCanvasElement(element); + + if (typeof element.getContext === 'undefined') { + throw CANVAS_INIT_ERROR; + } + }, + + /** + * @private + * @param {Object} [options] Options object + */ + _initOptions: function (options) { + for (var prop in options) { + this[prop] = options[prop]; + } + + this.width = 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'; + }, + + /** + * Creates a bottom canvas + * @private + * @param {HTMLElement} [canvasEl] + */ + _createLowerCanvas: function (canvasEl) { + this.lowerCanvasEl = fabric.util.getById(canvasEl) || this._createCanvasElement(); + this._initCanvasElement(this.lowerCanvasEl); + + fabric.util.addClass(this.lowerCanvasEl, 'lower-canvas'); + + if (this.interactive) { + this._applyCanvasStyle(this.lowerCanvasEl); + } + + this.contextContainer = this.lowerCanvasEl.getContext('2d'); + }, + + /** + * Returns canvas width (in px) + * @return {Number} + */ + getWidth: function () { + return this.width; + }, + + /** + * Returns canvas height (in px) + * @return {Number} + */ + getHeight: function () { + return this.height; + }, + + /** + * Sets width of this canvas instance + * @param {Number} width value to set width to + * @return {fabric.Canvas} instance + * @chainable true + */ + setWidth: function (value) { + return this._setDimension('width', value); + }, + + /** + * Sets height of this canvas instance + * @param {Number} height value to set height to + * @return {fabric.Canvas} instance + * @chainable true + */ + setHeight: function (value) { + return this._setDimension('height', value); + }, + + /** + * Sets dimensions (width, height) of this canvas instance + * @param {Object} dimensions Object with width/height properties + * @param {Number} [dimensions.width] Width of canvas element + * @param {Number} [dimensions.height] Height of canvas element + * @return {fabric.Canvas} thisArg + * @chainable + */ + setDimensions: function(dimensions) { + for (var prop in dimensions) { + this._setDimension(prop, dimensions[prop]); + } + return this; + }, + + /** + * Helper for setting width/height + * @private + * @param {String} prop property (width|height) + * @param {Number} value value to set property to + * @return {fabric.Canvas} instance + * @chainable true + */ + _setDimension: function (prop, value) { + this.lowerCanvasEl[prop] = value; + this.lowerCanvasEl.style[prop] = value + 'px'; + + if (this.upperCanvasEl) { + this.upperCanvasEl[prop] = value; + this.upperCanvasEl.style[prop] = value + 'px'; + } + + if (this.cacheCanvasEl) { + this.cacheCanvasEl[prop] = value; + } + + if (this.wrapperEl) { + this.wrapperEl.style[prop] = value + 'px'; + } + + this[prop] = value; + + this.calcOffset(); + this.renderAll(); + + return this; + }, + + /** + * Returns <canvas> element corresponding to this instance + * @return {HTMLCanvasElement} + */ + getElement: function () { + return this.lowerCanvasEl; + }, + + /** + * Returns currently selected object, if any + * @return {fabric.Object} + */ + getActiveObject: function() { + return null; + }, + + /** + * Returns currently selected group of object, if any + * @return {fabric.Group} + */ + getActiveGroup: function() { + return null; + }, + + /** + * Given a context, renders an object on that context + * @param {CanvasRenderingContext2D} ctx Context to render object on + * @param {fabric.Object} object Object to render + * @private + */ + _draw: function (ctx, object) { + if (!object) return; + + if (this.controlsAboveOverlay) { + var hasBorders = object.hasBorders, hasControls = object.hasControls; + object.hasBorders = object.hasControls = false; + object.render(ctx); + object.hasBorders = hasBorders; + object.hasControls = hasControls; + } + else { + object.render(ctx); + } + }, + + /** + * @private + * @param {fabric.Object} obj Object that was added + */ + _onObjectAdded: function(obj) { + this.stateful && obj.setupState(); + obj.setCoords(); + obj.canvas = this; + this.fire('object:added', { target: obj }); + obj.fire('added'); + }, + + /** + * @private + * @param {fabric.Object} obj Object that was removed + */ + _onObjectRemoved: function(obj) { + // 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'); + } + + this.fire('object:removed', { target: obj }); + obj.fire('removed'); + }, + + /** + * Clears specified context of canvas element + * @param {CanvasRenderingContext2D} ctx Context to clear + * @return {fabric.Canvas} thisArg + * @chainable + */ + clearContext: function(ctx) { + ctx.clearRect(0, 0, this.width, this.height); + return this; + }, + + /** + * Returns context of canvas where objects are drawn + * @return {CanvasRenderingContext2D} + */ + getContext: function () { + return this.contextContainer; + }, + + /** + * Clears all contexts (background, main, top) of an instance + * @return {fabric.Canvas} thisArg + * @chainable + */ + clear: function () { + this._objects.length = 0; + if (this.discardActiveGroup) { + this.discardActiveGroup(); + } + if (this.discardActiveObject) { + this.discardActiveObject(); + } + this.clearContext(this.contextContainer); + if (this.contextTop) { + this.clearContext(this.contextTop); + } + this.fire('canvas:cleared'); + this.renderAll(); + return this; + }, + + /** + * Renders both the top canvas and the secondary container canvas. + * @param {Boolean} [allOnTop] Whether we want to force all images to be rendered on the top canvas + * @return {fabric.Canvas} instance + * @chainable + */ + renderAll: function (allOnTop) { + + var canvasToDrawOn = this[(allOnTop === true && this.interactive) ? 'contextTop' : 'contextContainer']; + var activeGroup = this.getActiveGroup(); + + if (this.contextTop && this.selection && !this._groupSelector) { + this.clearContext(this.contextTop); + } + + if (!allOnTop) { + this.clearContext(canvasToDrawOn); + } + + this.fire('before:render'); + + if (this.clipTo) { + fabric.util.clipContext(this, canvasToDrawOn); + } + + this._renderBackground(canvasToDrawOn); + this._renderObjects(canvasToDrawOn, activeGroup); + this._renderActiveGroup(canvasToDrawOn, activeGroup); + + if (this.clipTo) { + canvasToDrawOn.restore(); + } + + this._renderOverlay(canvasToDrawOn); + + if (this.controlsAboveOverlay && this.interactive) { + this.drawControls(canvasToDrawOn); + } + + this.fire('after:render'); + + return this; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {fabric.Group} activeGroup + */ + _renderObjects: function(ctx, activeGroup) { + var i, length; + + // fast path + if (!activeGroup) { + for (i = 0, length = this._objects.length; i < length; ++i) { + this._draw(ctx, this._objects[i]); + } + } + else { + for (i = 0, length = this._objects.length; i < length; ++i) { + if (this._objects[i] && !activeGroup.contains(this._objects[i])) { + this._draw(ctx, this._objects[i]); + } + } + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {fabric.Group} activeGroup + */ + _renderActiveGroup: function(ctx, activeGroup) { + + // delegate rendering to group selection (if one exists) + if (activeGroup) { + + //Store objects in group preserving order, then replace + var sortedObjects = []; + this.forEachObject(function (object) { + if (activeGroup.contains(object)) { + sortedObjects.push(object); + } + }); + activeGroup._set('objects', sortedObjects); + this._draw(ctx, activeGroup); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderBackground: function(ctx) { + if (this.backgroundColor) { + ctx.fillStyle = this.backgroundColor.toLive + ? this.backgroundColor.toLive(ctx) + : this.backgroundColor; + + ctx.fillRect( + this.backgroundColor.offsetX || 0, + this.backgroundColor.offsetY || 0, + this.width, + this.height); + } + if (this.backgroundImage) { + this.backgroundImage.render(ctx); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderOverlay: function(ctx) { + if (this.overlayColor) { + ctx.fillStyle = this.overlayColor.toLive + ? this.overlayColor.toLive(ctx) + : this.overlayColor; + + ctx.fillRect( + this.overlayColor.offsetX || 0, + this.overlayColor.offsetY || 0, + this.width, + this.height); + } + if (this.overlayImage) { + this.overlayImage.render(ctx); + } + }, + + /** + * Method to render only the top canvas. + * Also used to render the group selection box. + * @return {fabric.Canvas} thisArg + * @chainable + */ + renderTop: function () { + var ctx = this.contextTop || this.contextContainer; + this.clearContext(ctx); + + // we render the top context - last object + if (this.selection && this._groupSelector) { + this._drawSelection(); + } + + // delegate rendering to group selection if one exists + // used for drawing selection borders/controls + var activeGroup = this.getActiveGroup(); + if (activeGroup) { + activeGroup.render(ctx); + } + + this._renderOverlay(ctx); + + this.fire('after:render'); + + return this; + }, + + /** + * Returns coordinates of a center of canvas. + * Returned value is an object with top and left properties + * @return {Object} object with "top" and "left" number values + */ + getCenter: function () { + return { + top: this.getHeight() / 2, + left: this.getWidth() / 2 + }; + }, + + /** + * Centers object horizontally. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @param {fabric.Object} object Object to center horizontally + * @return {fabric.Canvas} thisArg + */ + centerObjectH: function (object) { + this._centerObject(object, new fabric.Point(this.getCenter().left, object.getCenterPoint().y)); + this.renderAll(); + return this; + }, + + /** + * Centers object vertically. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @param {fabric.Object} object Object to center vertically + * @return {fabric.Canvas} thisArg + * @chainable + */ + centerObjectV: function (object) { + this._centerObject(object, new fabric.Point(object.getCenterPoint().x, this.getCenter().top)); + this.renderAll(); + return this; + }, + + /** + * Centers object vertically and horizontally. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @param {fabric.Object} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + centerObject: function(object) { + var center = this.getCenter(); + + this._centerObject(object, new fabric.Point(center.left, center.top)); + this.renderAll(); + return this; + }, + + /** + * @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'); + 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 activeGroup = this.getActiveGroup(); + if (activeGroup) { + this.discardActiveGroup(); + } + + var data = { + objects: this._toObjects(methodName, propertiesToInclude) + }; + + extend(data, this.__serializeBgOverlay()); + + fabric.util.populateWithProperties(this, data, propertiesToInclude); + + if (activeGroup) { + this.setActiveGroup(new fabric.Group(activeGroup.getObjects())); + activeGroup.forEachObject(function(o) { + o.set('active', true); + }); + } + return data; + }, + + /** + * @private + */ + _toObjects: function(methodName, propertiesToInclude) { + return this.getObjects().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() { + var data = { + background: (this.backgroundColor && this.backgroundColor.toObject) + ? this.backgroundColor.toObject() + : this.backgroundColor + }; + + if (this.overlayColor) { + data.overlay = this.overlayColor.toObject + ? this.overlayColor.toObject() + : this.overlayColor; + } + if (this.backgroundImage) { + data.backgroundImage = this.backgroundImage.toObject(); + } + if (this.overlayImage) { + data.overlayImage = this.overlayImage.toObject(); + } + + return data; + }, + + /* _TO_SVG_START_ */ + /** + * 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 {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'); + + this._setSVGObjects(markup, reviver); + + this._setSVGBgOverlayColor(markup, 'overlayColor'); + this._setSVGBgOverlayImage(markup, 'overlayImage'); + + markup.push(''); + + return markup.join(''); + }, + + /** + * @private + */ + _setSVGPreamble: function(markup, options) { + if (!options.suppressPreamble) { + markup.push( + '', + '\n' + ); + } + }, + + /** + * @private + */ + _setSVGHeader: function(markup, options) { + markup.push( + '', + 'Created with Fabric.js ', fabric.version, '', + '', + fabric.createSVGFontFacesMarkup(this.getObjects()), + fabric.createSVGRefElementsMarkup(this), + '' + ); + }, + + /** + * @private + */ + _setSVGObjects: function(markup, reviver) { + var activeGroup = this.getActiveGroup(); + if (activeGroup) { + this.discardActiveGroup(); + } + for (var i = 0, objects = this.getObjects(), len = objects.length; i < len; i++) { + markup.push(objects[i].toSVG(reviver)); + } + if (activeGroup) { + this.setActiveGroup(new fabric.Group(activeGroup.getObjects())); + activeGroup.forEachObject(function(o) { + o.set('active', true); + }); + } + }, + + /** + * @private + */ + _setSVGBgOverlayImage: function(markup, property) { + if (this[property] && this[property].toSVG) { + markup.push(this[property].toSVG()); + } + }, + + /** + * @private + */ + _setSVGBgOverlayColor: function(markup, property) { + if (this[property] && this[property].source) { + markup.push( + '' + ); + } + else if (this[property] && property === 'overlayColor') { + markup.push( + '' + ); + } + }, + /* _TO_SVG_END_ */ + + /** + * Moves an object to the bottom of the stack of drawn objects + * @param {fabric.Object} object Object to send to back + * @return {fabric.Canvas} thisArg + * @chainable + */ + sendToBack: function (object) { + removeFromArray(this._objects, object); + this._objects.unshift(object); + return this.renderAll && this.renderAll(); + }, + + /** + * Moves an object to the top of the stack of drawn objects + * @param {fabric.Object} object Object to send + * @return {fabric.Canvas} thisArg + * @chainable + */ + bringToFront: function (object) { + removeFromArray(this._objects, object); + this._objects.push(object); + return this.renderAll && this.renderAll(); + }, + + /** + * Moves an object down in stack of drawn objects + * @param {fabric.Object} object Object to send + * @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object + * @return {fabric.Canvas} thisArg + * @chainable + */ + sendBackwards: function (object, intersecting) { + var idx = this._objects.indexOf(object); + + // if object is not on the bottom of stack + if (idx !== 0) { + var newIdx = 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 up in stack of drawn objects + * @param {fabric.Object} object Object to send + * @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object + * @return {fabric.Canvas} thisArg + * @chainable + */ + bringForward: function (object, intersecting) { + var idx = this._objects.indexOf(object); + + // if object is not on top of stack (last item in an array) + if (idx !== this._objects.length-1) { + var newIdx = 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(); + this.interactive && this.removeListeners(); + 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 methodName {String} Method to check support for; + * Could be one of "getImageData", "toDataURL", "toDataURLWithQuality" or "setLineDash" + * @return {Boolean | null} `true` if method is supported (or at least exists), + * `null` if canvas element or context can not be initialized + */ + supports: function (methodName) { + var el = fabric.util.createCanvasElement(); + + if (!el || !el.getContext) { + return null; + } + + var ctx = el.getContext('2d'); + if (!ctx) { + return null; + } + + switch (methodName) { + + case 'getImageData': + return typeof ctx.getImageData !== 'undefined'; + + case 'setLineDash': + return typeof ctx.setLineDash !== 'undefined'; + + case 'toDataURL': + return typeof el.toDataURL !== 'undefined'; + + case 'toDataURLWithQuality': + try { + el.toDataURL('image/jpeg', 0); + return true; + } + catch (e) { } + return false; + + default: + return null; + } + } + }); + + /** + * 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', + + /** + * Sets shadow of an object + * @param {Object|String} [options] Options object or string (e.g. "2px 2px 10px rgba(0,0,0,0.2)") + * @return {fabric.Object} thisArg + * @chainable + */ + setShadow: function(options) { + this.shadow = new fabric.Shadow(options); + return this; + }, + + /** + * Sets brush styles + * @private + */ + _setBrushStyles: function() { + var ctx = this.canvas.contextTop; + + ctx.strokeStyle = this.color; + ctx.lineWidth = this.width; + ctx.lineCap = this.strokeLineCap; + ctx.lineJoin = this.strokeLineJoin; + }, + + /** + * Sets brush shadow styles + * @private + */ + _setShadow: function() { + if (!this.shadow) return; + + var ctx = this.canvas.contextTop; + + ctx.shadowColor = this.shadow.color; + ctx.shadowBlur = this.shadow.blur; + ctx.shadowOffsetX = this.shadow.offsetX; + ctx.shadowOffsetY = this.shadow.offsetY; + }, + + /** + * Removes brush shadow styles + * @private + */ + _resetShadow: function() { + var ctx = this.canvas.contextTop; + + ctx.shadowColor = ''; + ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0; + } +}); + + +(function() { + + var utilMin = fabric.util.array.min, + utilMax = fabric.util.array.max; + + /** + * PencilBrush class + * @class fabric.PencilBrush + * @extends fabric.BaseBrush + */ + fabric.PencilBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric.PencilBrush.prototype */ { + + /** + * Constructor + * @param {fabric.Canvas} canvas + * @return {fabric.PencilBrush} Instance of a pencil brush + */ + initialize: function(canvas) { + this.canvas = canvas; + this._points = [ ]; + }, + + /** + * Inovoked on mouse down + * @param {Object} pointer + */ + onMouseDown: function(pointer) { + this._prepareForDrawing(pointer); + // capture coordinates immediately + // this allows to draw dots (when movement never occurs) + this._captureDrawingPath(pointer); + this._render(); + }, + + /** + * Inovoked on mouse move + * @param {Object} pointer + */ + onMouseMove: function(pointer) { + this._captureDrawingPath(pointer); + // redraw curve + // clear top canvas + this.canvas.clearContext(this.canvas.contextTop); + this._render(); + }, + + /** + * Invoked on mouse up + */ + onMouseUp: function() { + this._finalizeAndAddPath(); + }, + + /** + * @param {Object} pointer + */ + _prepareForDrawing: function(pointer) { + + var p = new fabric.Point(pointer.x, pointer.y); + + this._reset(); + this._addPoint(p); + + this.canvas.contextTop.moveTo(p.x, p.y); + }, + + /** + * @private + * @param {fabric.Point} point + */ + _addPoint: function(point) { + this._points.push(point); + }, + + /** + * Clear points array and set contextTop canvas + * style. + * + * @private + * + */ + _reset: function() { + this._points.length = 0; + + this._setBrushStyles(); + this._setShadow(); + }, + + /** + * @private + * + * @param point {pointer} (fabric.util.pointer) actual mouse position + * related to the canvas. + */ + _captureDrawingPath: function(pointer) { + var pointerPoint = new fabric.Point(pointer.x, pointer.y); + this._addPoint(pointerPoint); + }, + + /** + * Draw a smooth path on the topCanvas using quadraticCurveTo + * + * @private + */ + _render: function() { + var ctx = this.canvas.contextTop; + ctx.beginPath(); + + var p1 = this._points[0]; + var p2 = this._points[1]; + + //if we only have 2 points in the path and they are the same + //it means that the user only clicked the canvas without moving the mouse + //then we should be drawing a dot. A path isn't drawn between two identical dots + //that's why we set them apart a bit + if (this._points.length === 2 && p1.x === p2.x && p1.y === p2.y) { + p1.x -= 0.5; + p2.x += 0.5; + } + ctx.moveTo(p1.x, p1.y); + + for (var i = 1, len = this._points.length; i < len; i++) { + // we pick the point between pi+1 & pi+2 as the + // end point and p1 as our control point. + var midPoint = p1.midPointFrom(p2); + ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y); + + p1 = this._points[i]; + p2 = this._points[i+1]; + } + // Draw last line as a straight line while + // we wait for the next point to be able to calculate + // the bezier control point + ctx.lineTo(p1.x, p1.y); + ctx.stroke(); + }, + + /** + * Return an SVG path based on our captured points and their bounding box + * + * @private + */ + _getSVGPathData: function() { + this.box = this.getPathBoundingBox(this._points); + return this.convertPointsToSVGPath( + this._points, this.box.minx, this.box.maxx, this.box.miny, this.box.maxy); + }, + + /** + * Returns bounding box of a path based on given points + * @param {Array} points + * @return {Object} object with minx, miny, maxx, maxy + */ + getPathBoundingBox: function(points) { + var xBounds = [], + yBounds = [], + p1 = points[0], + p2 = points[1], + startPoint = p1; + + for (var i = 1, len = points.length; i < len; i++) { + var midPoint = p1.midPointFrom(p2); + // with startPoint, p1 as control point, midpoint as end point + xBounds.push(startPoint.x); + xBounds.push(midPoint.x); + yBounds.push(startPoint.y); + yBounds.push(midPoint.y); + + p1 = points[i]; + p2 = points[i+1]; + startPoint = midPoint; + } // end for + + xBounds.push(p1.x); + yBounds.push(p1.y); + + return { + minx: utilMin(xBounds), + miny: utilMin(yBounds), + maxx: utilMax(xBounds), + maxy: utilMax(yBounds) + }; + }, + + /** + * Converts points to SVG path + * @param {Array} points Array of points + * @return {String} SVG path + */ + convertPointsToSVGPath: function(points, minX, maxX, minY) { + var path = []; + var p1 = new fabric.Point(points[0].x - minX, points[0].y - minY); + var p2 = new fabric.Point(points[1].x - minX, points[1].y - minY); + + path.push('M ', points[0].x - minX, ' ', points[0].y - minY, ' '); + for (var i = 1, len = points.length; i < len; i++) { + var midPoint = p1.midPointFrom(p2); + // p1 is our bezier control point + // midpoint is our endpoint + // start point is p(i-1) value. + path.push('Q ', p1.x, ' ', p1.y, ' ', midPoint.x, ' ', midPoint.y, ' '); + p1 = new fabric.Point(points[i].x - minX, points[i].y - minY); + if ((i+1) < points.length) { + p2 = new fabric.Point(points[i+1].x - minX, points[i+1].y - minY); + } + } + path.push('L ', p1.x, ' ', p1.y, ' '); + return path; + }, + + /** + * Creates fabric.Path object to add on canvas + * @param {String} pathData Path data + * @return {fabric.Path} path to add on canvas + */ + createPath: function(pathData) { + var path = new fabric.Path(pathData); + path.fill = null; + path.stroke = this.color; + path.strokeWidth = this.width; + path.strokeLineCap = this.strokeLineCap; + path.strokeLineJoin = this.strokeLineJoin; + + if (this.shadow) { + this.shadow.affectStroke = true; + path.setShadow(this.shadow); + } + + return path; + }, + + /** + * On mouseup after drawing the path on contextTop canvas + * we use the points captured to create an new fabric path object + * and add it to the fabric canvas. + * + */ + _finalizeAndAddPath: function() { + var ctx = this.canvas.contextTop; + ctx.closePath(); + + var pathData = this._getSVGPathData().join(''); + if (pathData === "M 0 0 Q 0 0 0 0 L 0 0") { + // do not create 0 width/height paths, as they are + // rendered inconsistently across browsers + // Firefox 4, for example, renders a dot, + // whereas Chrome 10 renders nothing + this.canvas.renderAll(); + return; + } + + // set path origin coordinates based on our bounding box + var originLeft = this.box.minx + (this.box.maxx - this.box.minx) /2; + var originTop = this.box.miny + (this.box.maxy - this.box.miny) /2; + + this.canvas.contextTop.arc(originLeft, originTop, 3, 0, Math.PI * 2, false); + + var path = this.createPath(pathData); + path.set({ + left: originLeft, + top: originTop, + originX: 'center', + originY: 'center' + }); + + this.canvas.add(path); + path.setCoords(); + + this.canvas.clearContext(this.canvas.contextTop); + this._resetShadow(); + this.canvas.renderAll(); + + // fire event 'path' created + this.canvas.fire('path:created', { path: path }); + } + }); +})(); + + +/** + * CircleBrush class + * @class fabric.CircleBrush + */ +fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric.CircleBrush.prototype */ { + + /** + * Width of a brush + * @type Number + * @default + */ + width: 10, + + /** + * Constructor + * @param {fabric.Canvas} canvas + * @return {fabric.CircleBrush} Instance of a circle brush + */ + initialize: function(canvas) { + this.canvas = canvas; + this.points = [ ]; + }, + /** + * Invoked inside on mouse down and mouse move + * @param {Object} pointer + */ + drawDot: function(pointer) { + var point = this.addPoint(pointer); + var ctx = this.canvas.contextTop; + + ctx.fillStyle = point.fill; + ctx.beginPath(); + ctx.arc(point.x, point.y, point.radius, 0, Math.PI * 2, false); + ctx.closePath(); + ctx.fill(); + }, + + /** + * Invoked on mouse down + */ + onMouseDown: function(pointer) { + this.points.length = 0; + this.canvas.clearContext(this.canvas.contextTop); + this._setShadow(); + this.drawDot(pointer); + }, + + /** + * Invoked on mouse move + * @param {Object} pointer + */ + onMouseMove: function(pointer) { + this.drawDot(pointer); + }, + + /** + * Invoked on mouse up + */ + onMouseUp: function() { + var originalRenderOnAddRemove = this.canvas.renderOnAddRemove; + this.canvas.renderOnAddRemove = false; + + var circles = [ ]; + + for (var i = 0, len = this.points.length; i < len; i++) { + var point = this.points[i]; + var circle = new fabric.Circle({ + radius: point.radius, + left: point.x, + top: point.y, + 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' }); + + this.canvas.add(group); + this.canvas.fire('path:created', { path: group }); + + this.canvas.clearContext(this.canvas.contextTop); + this._resetShadow(); + this.canvas.renderOnAddRemove = originalRenderOnAddRemove; + this.canvas.renderAll(); + }, + + /** + * @param {Object} pointer + * @return {fabric.Point} Just added pointer point + */ + addPoint: function(pointer) { + var pointerPoint = new fabric.Point(pointer.x, pointer.y); + + var circleRadius = fabric.util.getRandomInt( + Math.max(0, this.width - 20), this.width + 20) / 2; + + var circleColor = new fabric.Color(this.color) + .setAlpha(fabric.util.getRandomInt(0, 100) / 100) + .toRgba(); + + pointerPoint.radius = circleRadius; + pointerPoint.fill = circleColor; + + this.points.push(pointerPoint); + + return pointerPoint; + } +}); + + +/** + * SprayBrush class + * @class fabric.SprayBrush + */ +fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric.SprayBrush.prototype */ { + + /** + * Width of a spray + * @type Number + * @default + */ + width: 10, + + /** + * Density of a spray (number of dots per chunk) + * @type Number + * @default + */ + density: 20, + + /** + * Width of spray dots + * @type Number + * @default + */ + dotWidth: 1, + + /** + * Width variance of spray dots + * @type Number + * @default + */ + dotWidthVariance: 1, + + /** + * Whether opacity of a dot should be random + * @type Boolean + * @default + */ + randomOpacity: false, + + /** + * 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' }); + 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(); + }, + + _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; + ctx.save(); + + for (var i = 0, len = this.sprayChunkPoints.length; i < len; i++) { + var point = this.sprayChunkPoints[i]; + if (typeof point.opacity !== 'undefined') { + ctx.globalAlpha = point.opacity; + } + ctx.fillRect(point.x, point.y, point.width, point.width); + } + ctx.restore(); + }, + + /** + * @param {Object} pointer + */ + addSprayChunk: function(pointer) { + this.sprayChunkPoints = [ ]; + + var x, y, width, radius = this.width / 2; + + for (var i = 0; i < this.density; i++) { + + x = fabric.util.getRandomInt(pointer.x - radius, pointer.x + radius); + y = fabric.util.getRandomInt(pointer.y - radius, pointer.y + radius); + + if (this.dotWidthVariance) { + width = fabric.util.getRandomInt( + // bottom clamp width to 1 + Math.max(1, this.dotWidth - this.dotWidthVariance), + this.dotWidth + this.dotWidthVariance); + } + else { + width = this.dotWidth; + } + + var point = { x: x, y: y, width: width }; + + if (this.randomOpacity) { + point.opacity = fabric.util.getRandomInt(0, 100) / 100; + } + + this.sprayChunkPoints.push(point); + } + + this.sprayChunks.push(this.sprayChunkPoints); + } +}); + + +/** + * PatternBrush class + * @class fabric.PatternBrush + * @extends fabric.BaseBrush + */ +fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fabric.PatternBrush.prototype */ { + + getPatternSrc: function() { + + var dotWidth = 20, + dotDistance = 5, + patternCanvas = fabric.document.createElement('canvas'), + patternCtx = patternCanvas.getContext('2d'); + + patternCanvas.width = patternCanvas.height = dotWidth + dotDistance; + + patternCtx.fillStyle = this.color; + patternCtx.beginPath(); + patternCtx.arc(dotWidth / 2, dotWidth / 2, dotWidth / 2, 0, Math.PI * 2, false); + patternCtx.closePath(); + patternCtx.fill(); + + return patternCanvas; + }, + + getPatternSrcFunction: function() { + return String(this.getPatternSrc).replace('this.color', '"' + this.color + '"'); + }, + + /** + * Creates "pattern" instance property + */ + getPattern: function() { + return this.canvas.contextTop.createPattern(this.source || this.getPatternSrc(), 'repeat'); + }, + + /** + * Sets brush styles + */ + _setBrushStyles: function() { + this.callSuper('_setBrushStyles'); + this.canvas.contextTop.strokeStyle = this.getPattern(); + }, + + /** + * Creates path + */ + createPath: function(pathData) { + var path = this.callSuper('createPath', pathData); + path.stroke = new fabric.Pattern({ + source: this.source || this.getPatternSrcFunction() + }); + return path; + } +}); + + +(function() { + + var getPointer = fabric.util.getPointer, + degreesToRadians = fabric.util.degreesToRadians, + radiansToDegrees = fabric.util.radiansToDegrees, + atan2 = Math.atan2, + abs = Math.abs, + + 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: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(); + + fabric.Canvas.activeInstance = this; + }, + + /** + * When true, objects can be transformed by one side (unproportionally) + * @type Boolean + * @default + */ + uniScaleTransform: false, + + /** + * When true, objects use center point as the origin of 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 that canvas is interactive. This property should not be changed. + * @type Boolean + * @default + */ + interactive: true, + + /** + * Indicates whether group selection should be enabled + * @type Boolean + * @default + */ + selection: true, + + /** + * Color of selection + * @type String + * @default + */ + selectionColor: 'rgba(100, 100, 255, 0.3)', // blue + + /** + * Default dash array pattern + * If not empty the selection border is dashed + * @type Array + */ + selectionDashArray: [ ], + + /** + * Color of the border of selection (usually slightly darker than color of selection itself) + * @type String + * @default + */ + selectionBorderColor: 'rgba(255, 255, 255, 0.3)', + + /** + * Width of a line used in object/group selection + * @type Number + * @default + */ + selectionLineWidth: 1, + + /** + * Default cursor value used when hovering over an object on canvas + * @type String + * @default + */ + hoverCursor: 'move', + + /** + * Default cursor value used when moving an object on canvas + * @type String + * @default + */ + moveCursor: 'move', + + /** + * Default cursor value used for the entire canvas + * @type String + * @default + */ + defaultCursor: 'default', + + /** + * Cursor value used during free drawing + * @type String + * @default + */ + freeDrawingCursor: 'crosshair', + + /** + * Cursor value used for rotation point + * @type String + * @default + */ + rotationCursor: 'crosshair', + + /** + * Default element class that's given to wrapper (div) element of canvas + * @type String + * @default + */ + containerClass: 'canvas-container', + + /** + * When true, object detection happens on per-pixel basis rather than on per-bounding-box + * @type Boolean + * @default + */ + perPixelTargetFind: false, + + /** + * Number of pixels around target pixel to tolerate (consider active) during object detection + * @type Number + * @default + */ + targetFindTolerance: 0, + + /** + * When true, target detection is skipped when hovering over canvas. This can be used to improve performance. + * @type Boolean + * @default + */ + skipTargetFind: false, + + /** + * @private + */ + _initInteractive: function() { + this._currentTransform = null; + this._groupSelector = null; + this._initWrapperElement(); + this._createUpperCanvas(); + this._initEventListeners(); + + this.freeDrawingBrush = fabric.PencilBrush && new fabric.PencilBrush(this); + + this.calcOffset(); + }, + + /** + * Resets the current transform to its original values and chooses the type of resizing based on the event + * @private + * @param {Event} e Event object fired on mousemove + */ + _resetCurrentTransform: function(e) { + var t = this._currentTransform; + + t.target.set({ + 'scaleX': t.original.scaleX, + 'scaleY': t.original.scaleY, + 'left': t.original.left, + 'top': t.original.top + }); + + if (this._shouldCenterTransform(e, 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 + * @return {Boolean} true if point is contained within an area of given object + */ + containsPoint: function (e, target) { + var pointer = this.getPointer(e), + xy = this._normalizePointer(target, pointer); + + // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html + // http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html + return (target.containsPoint(xy) || target._findTargetCorner(e, this._offset)); + }, + + /** + * @private + */ + _normalizePointer: function (object, pointer) { + var activeGroup = this.getActiveGroup(), + x = pointer.x, + y = pointer.y; + + var isObjectInGroup = ( + activeGroup && + object.type !== 'group' && + activeGroup.contains(object) + ); + + if (isObjectInGroup) { + x -= activeGroup.left; + y -= activeGroup.top; + } + return { x: x, y: y }; + }, + + /** + * Returns true if object is transparent at a certain location + * @param {fabric.Object} target Object to check + * @param {Number} x Left coordinate + * @param {Number} y Top coordinate + * @return {Boolean} + */ + isTargetTransparent: function (target, x, y) { + var hasBorders = target.hasBorders, + transparentCorners = target.transparentCorners; + + target.hasBorders = target.transparentCorners = false; + + this._draw(this.contextCache, target); + + target.hasBorders = hasBorders; + target.transparentCorners = transparentCorners; + + var isTransparent = fabric.util.isTransparent( + this.contextCache, x, y, this.targetFindTolerance); + + this.clearContext(this.contextCache); + + 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.shiftKey) + || + (target && !target.evented) + || + (target && + !target.selectable && + activeObject && + activeObject !== target) + ); + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _shouldCenterTransform: function (e, 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 ? !e.altKey : e.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) { + var action = 'drag'; + if (corner) { + action = (corner === 'ml' || corner === 'mr') + ? 'scaleX' + : (corner === 'mt' || corner === 'mb') + ? 'scaleY' + : corner === 'mtr' + ? 'rotate' + : 'scale'; + } + return action; + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _setupCurrentTransform: function (e, target) { + if (!target) return; + + var corner = target._findTargetCorner(e, this._offset), + pointer = getPointer(e, target.canvas.upperCanvasEl), + action = this._getActionFromCorner(target, corner), + origin = this._getOriginFromCorner(target, corner); + + this._currentTransform = { + target: target, + action: action, + scaleX: target.scaleX, + scaleY: target.scaleY, + offsetX: pointer.x - target.left, + offsetY: pointer.y - target.top, + originX: origin.x, + originY: origin.y, + ex: pointer.x, + ey: pointer.y, + left: target.left, + top: target.top, + theta: degreesToRadians(target.angle), + width: target.width * target.scaleX, + mouseXSign: 1, + mouseYSign: 1 + }; + + this._currentTransform.original = { + left: target.left, + top: target.top, + scaleX: target.scaleX, + scaleY: target.scaleY, + originX: origin.x, + originY: origin.y + }; + + this._resetCurrentTransform(e); + }, + + /** + * Translates object by "setting" its left/top + * @private + * @param x {Number} pointer's x coordinate + * @param y {Number} pointer's y coordinate + */ + _translateObject: function (x, y) { + var target = this._currentTransform.target; + + if (!target.get('lockMovementX')) { + target.set('left', x - this._currentTransform.offsetX); + } + if (!target.get('lockMovementY')) { + target.set('top', y - this._currentTransform.offsetY); + } + }, + + /** + * Scales object by invoking its scaleX/scaleY methods + * @private + * @param x {Number} pointer's x coordinate + * @param y {Number} pointer's y coordinate + * @param by {String} Either 'x' or 'y' - specifies dimension constraint by which to scale an object. + * When not provided, an object is scaled by both dimensions equally + */ + _scaleObject: function (x, y, by) { + var t = this._currentTransform, + offset = this._offset, + target = t.target, + lockScalingX = target.get('lockScalingX'), + lockScalingY = target.get('lockScalingY'); + + if (lockScalingX && lockScalingY) return; + + // Get the constraint point + var constraintPosition = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY); + var localMouse = target.toLocalPoint(new fabric.Point(x - offset.left, y - offset.top), t.originX, t.originY); + + this._setLocalMouse(localMouse, t); + + // Actually scale the object + this._setObjectScale(localMouse, t, lockScalingX, lockScalingY, by); + + // Make sure the constraints apply + target.setPositionByOrigin(constraintPosition, t.originX, t.originY); + }, + + /** + * @private + */ + _setObjectScale: function(localMouse, transform, lockScalingX, lockScalingY, by) { + var target = transform.target; + + transform.newScaleX = target.scaleX; + transform.newScaleY = target.scaleY; + + if (by === 'equally' && !lockScalingX && !lockScalingY) { + this._scaleObjectEqually(localMouse, target, transform); + } + else if (!by) { + transform.newScaleX = localMouse.x / (target.width + target.strokeWidth); + transform.newScaleY = localMouse.y / (target.height + target.strokeWidth); + + lockScalingX || target.set('scaleX', transform.newScaleX); + lockScalingY || target.set('scaleY', transform.newScaleY); + } + else if (by === 'x' && !target.get('lockUniScaling')) { + transform.newScaleX = localMouse.x / (target.width + target.strokeWidth); + lockScalingX || target.set('scaleX', transform.newScaleX); + } + else if (by === 'y' && !target.get('lockUniScaling')) { + transform.newScaleY = localMouse.y / (target.height + target.strokeWidth); + lockScalingY || target.set('scaleY', transform.newScaleY); + } + + this._flipObject(transform); + }, + + /** + * @private + */ + _scaleObjectEqually: function(localMouse, target, transform) { + + var dist = localMouse.y + localMouse.x; + + var lastDist = (target.height + (target.strokeWidth)) * transform.original.scaleY + + (target.width + (target.strokeWidth)) * transform.original.scaleX; + + // 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; + + target.set('scaleX', transform.newScaleX); + target.set('scaleY', transform.newScaleY); + }, + + /** + * @private + */ + _flipObject: function(transform) { + if (transform.newScaleX < 0) { + if (transform.originX === 'left') { + transform.originX = 'right'; + } + else if (transform.originX === 'right') { + transform.originX = 'left'; + } + } + + if (transform.newScaleY < 0) { + if (transform.originY === 'top') { + transform.originY = 'bottom'; + } + else if (transform.originY === 'bottom') { + transform.originY = 'top'; + } + } + }, + + /** + * @private + */ + _setLocalMouse: function(localMouse, t) { + var target = t.target; + + if (t.originX === 'right') { + localMouse.x *= -1; + } + else if (t.originX === 'center') { + localMouse.x *= t.mouseXSign * 2; + + if (localMouse.x < 0) { + t.mouseXSign = -t.mouseXSign; + } + } + + if (t.originY === 'bottom') { + localMouse.y *= -1; + } + else if (t.originY === 'center') { + localMouse.y *= t.mouseYSign * 2; + + if (localMouse.y < 0) { + t.mouseYSign = -t.mouseYSign; + } + } + + // adjust the mouse coordinates when dealing with padding + if (abs(localMouse.x) > target.padding) { + if (localMouse.x < 0) { + localMouse.x += target.padding; + } + else { + localMouse.x -= target.padding; + } + } + else { // mouse is within the padding, set to 0 + localMouse.x = 0; + } + + if (abs(localMouse.y) > target.padding) { + if (localMouse.y < 0) { + localMouse.y += target.padding; + } + else { + localMouse.y -= target.padding; + } + } + else { + localMouse.y = 0; + } + }, + + /** + * Rotates object by invoking its rotate method + * @private + * @param x {Number} pointer's x coordinate + * @param y {Number} pointer's y coordinate + */ + _rotateObject: function (x, y) { + + var t = this._currentTransform, + o = this._offset; + + if (t.target.get('lockRotation')) return; + + var lastAngle = atan2(t.ey - t.top - o.top, t.ex - t.left - o.left), + curAngle = atan2(y - t.top - o.top, x - t.left - o.left), + angle = radiansToDegrees(curAngle - lastAngle + t.theta); + + // normalize angle to positive value + if (angle < 0) { + angle = 360 + angle; + } + + t.target.angle = angle; + }, + + /** + * @private + */ + _setCursor: function (value) { + this.upperCanvasEl.style.cursor = value; + }, + + /** + * @private + */ + _resetObjectTransform: function (target) { + target.scaleX = 1; + target.scaleY = 1; + target.setAngle(0); + }, + + /** + * @private + */ + _drawSelection: function () { + var ctx = this.contextTop, + groupSelector = this._groupSelector, + left = groupSelector.left, + top = groupSelector.top, + aleft = abs(left), + atop = abs(top); + + ctx.fillStyle = this.selectionColor; + + ctx.fillRect( + groupSelector.ex - ((left > 0) ? 0 : -left), + groupSelector.ey - ((top > 0) ? 0 : -top), + aleft, + atop + ); + + ctx.lineWidth = this.selectionLineWidth; + ctx.strokeStyle = this.selectionBorderColor; + + // selection border + if (this.selectionDashArray.length > 1) { + + var px = groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0: aleft); + var py = groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0: atop); + + ctx.beginPath(); + + fabric.util.drawDashedLine(ctx, px, py, px+aleft, py, this.selectionDashArray); + fabric.util.drawDashedLine(ctx, px, py+atop-1, px+aleft, py+atop-1, this.selectionDashArray); + fabric.util.drawDashedLine(ctx, px, py, px, py+atop, this.selectionDashArray); + fabric.util.drawDashedLine(ctx, px+aleft-1, py, px+aleft-1, py+atop, this.selectionDashArray); + + ctx.closePath(); + ctx.stroke(); + } + else { + ctx.strokeRect( + groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0 : aleft), + groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0 : atop), + aleft, + atop + ); + } + }, + + /** + * @private + */ + _isLastRenderedObject: function(e) { + return ( + this.controlsAboveOverlay && + this.lastRenderedObjectWithControlsAboveOverlay && + this.lastRenderedObjectWithControlsAboveOverlay.visible && + this.containsPoint(e, this.lastRenderedObjectWithControlsAboveOverlay) && + this.lastRenderedObjectWithControlsAboveOverlay._findTargetCorner(e, this._offset)); + }, + + /** + * Method that determines what object we are clicking on + * @param {Event} e mouse event + * @param {Boolean} skipGroup when true, group is skipped and only objects are traversed through + */ + findTarget: function (e, skipGroup) { + if (this.skipTargetFind) return; + + if (this._isLastRenderedObject(e)) { + return this.lastRenderedObjectWithControlsAboveOverlay; + } + + // first check current group (if one exists) + var activeGroup = this.getActiveGroup(); + if (activeGroup && !skipGroup && this.containsPoint(e, activeGroup)) { + return activeGroup; + } + + var target = this._searchPossibleTargets(e); + this._fireOverOutEvents(target); + return target; + }, + + /** + * @private + */ + _fireOverOutEvents: function(target) { + if (target) { + if (this._hoveredTarget !== target) { + this.fire('mouse:over', { target: target }); + target.fire('mouseover'); + if (this._hoveredTarget) { + this.fire('mouse:out', { target: this._hoveredTarget }); + this._hoveredTarget.fire('mouseout'); + } + this._hoveredTarget = target; + } + } + else if (this._hoveredTarget) { + this.fire('mouse:out', { target: this._hoveredTarget }); + this._hoveredTarget.fire('mouseout'); + this._hoveredTarget = null; + } + }, + + /** + * @private + */ + _searchPossibleTargets: function(e) { + + // Cache all targets where their bounding box contains point. + var possibleTargets = [], + target, + pointer = this.getPointer(e); + + for (var i = this._objects.length; i--; ) { + if (this._objects[i] && + this._objects[i].visible && + this._objects[i].evented && + this.containsPoint(e, this._objects[i])) { + + if (this.perPixelTargetFind || this._objects[i].perPixelTargetFind) { + possibleTargets[possibleTargets.length] = this._objects[i]; + } + else { + target = this._objects[i]; + this.relatedTarget = target; + break; + } + } + } + + for (var j = 0, len = possibleTargets.length; j < len; j++) { + pointer = this.getPointer(e); + var isTransparent = this.isTargetTransparent(possibleTargets[j], pointer.x, pointer.y); + if (!isTransparent) { + target = possibleTargets[j]; + this.relatedTarget = target; + break; + } + } + + return target; + }, + + /** + * Returns pointer coordinates relative to canvas. + * @param {Event} e + * @return {Object} object with "x" and "y" number values + */ + getPointer: function (e) { + var pointer = getPointer(e, this.upperCanvasEl); + return { + x: pointer.x - this._offset.left, + y: pointer.y - this._offset.top + }; + }, + + /** + * @private + * @param {HTMLElement|String} canvasEl Canvas element + * @throws {CANVAS_INIT_ERROR} If canvas can not be initialized + */ + _createUpperCanvas: function () { + var lowerCanvasClass = this.lowerCanvasEl.className.replace(/\s*lower-canvas\s*/, ''); + + this.upperCanvasEl = this._createCanvasElement(); + fabric.util.addClass(this.upperCanvasEl, 'upper-canvas ' + lowerCanvasClass); + + this.wrapperEl.appendChild(this.upperCanvasEl); + + this._copyCanvasStyle(this.lowerCanvasEl, this.upperCanvasEl); + this._applyCanvasStyle(this.upperCanvasEl); + this.contextTop = this.upperCanvasEl.getContext('2d'); + }, + + /** + * @private + */ + _createCacheCanvas: function () { + this.cacheCanvasEl = this._createCanvasElement(); + this.cacheCanvasEl.setAttribute('width', this.width); + this.cacheCanvasEl.setAttribute('height', this.height); + this.contextCache = this.cacheCanvasEl.getContext('2d'); + }, + + /** + * @private + * @param {Number} width + * @param {Number} height + */ + _initWrapperElement: function () { + this.wrapperEl = fabric.util.wrapElement(this.lowerCanvasEl, 'div', { + 'class': this.containerClass + }); + fabric.util.setStyle(this.wrapperEl, { + width: this.getWidth() + 'px', + height: this.getHeight() + 'px', + position: 'relative' + }); + fabric.util.makeElementUnselectable(this.wrapperEl); + }, + + /** + * @private + * @param {Element} element + */ + _applyCanvasStyle: function (element) { + var width = this.getWidth() || element.width, + height = this.getHeight() || element.height; + + fabric.util.setStyle(element, { + position: 'absolute', + width: width + 'px', + height: height + 'px', + left: 0, + top: 0 + }); + element.width = width; + element.height = height; + fabric.util.makeElementUnselectable(element); + }, + + /** + * Copys the the entire inline style from one element (fromEl) to another (toEl) + * @private + * @param {Element} fromEl Element style is copied from + * @param {Element} toEl Element copied style is applied to + */ + _copyCanvasStyle: function (fromEl, toEl) { + toEl.style.cssText = fromEl.style.cssText; + }, + + /** + * Returns context of canvas where object selection is drawn + * @return {CanvasRenderingContext2D} + */ + getSelectionContext: function() { + return this.contextTop; + }, + + /** + * Returns <canvas> element on which object selection is drawn + * @return {HTMLCanvasElement} + */ + getSelectionElement: function () { + return this.upperCanvasEl; + }, + + /** + * @private + * @param {Object} object + */ + _setActiveObject: function(object) { + if (this._activeObject) { + this._activeObject.set('active', false); + } + 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) { + 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 + */ + _discardActiveObject: function() { + if (this._activeObject) { + this._activeObject.set('active', false); + } + this._activeObject = null; + }, + + /** + * Discards currently active object + * @return {fabric.Canvas} thisArg + * @chainable + */ + discardActiveObject: function (e) { + this._discardActiveObject(); + this.renderAll(); + this.fire('selection:cleared', { e: e }); + return this; + }, + + /** + * @private + * @param {fabric.Group} group + */ + _setActiveGroup: function(group) { + this._activeGroup = group; + if (group) { + group.canvas = this; + group.set('active', true); + } + }, + + /** + * Sets active group to a speicified one + * @param {fabric.Group} group Group to set as a current one + * @return {fabric.Canvas} thisArg + * @chainable + */ + setActiveGroup: function (group, 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 + * @return {fabric.Canvas} thisArg + */ + discardActiveGroup: function (e) { + 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 + */ + deactivateAll: function () { + var allObjects = this.getObjects(), + i = 0, + len = allObjects.length; + for ( ; i < len; i++) { + allObjects[i].set('active', false); + } + this._discardActiveGroup(); + this._discardActiveObject(); + return this; + }, + + /** + * Deactivates all objects and dispatches appropriate events + * @return {fabric.Canvas} thisArg + */ + deactivateAllWithDispatch: function (e) { + var activeObject = this.getActiveGroup() || this.getActiveObject(); + if (activeObject) { + this.fire('before:selection:cleared', { target: activeObject, e: e }); + } + this.deactivateAll(); + if (activeObject) { + this.fire('selection:cleared', { e: e }); + } + return this; + }, + + /** + * Draws objects' controls (borders/controls) + * @param {CanvasRenderingContext2D} ctx Context to render controls on + */ + drawControls: function(ctx) { + var activeGroup = this.getActiveGroup(); + if (activeGroup) { + this._drawGroupControls(ctx, activeGroup); + } + else { + this._drawObjectsControls(ctx); + } + }, + + /** + * @private + */ + _drawGroupControls: function(ctx, activeGroup) { + this._drawControls(ctx, activeGroup, 'Group'); + }, + + /** + * @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._drawControls(ctx, this._objects[i], 'Object'); + this.lastRenderedObjectWithControlsAboveOverlay = this._objects[i]; + } + }, + + /** + * @private + */ + _drawControls: function(ctx, object, klass) { + ctx.save(); + fabric[klass].prototype.transform.call(object, ctx); + object.drawBorders(ctx).drawControls(ctx); + ctx.restore(); + } + }); + + // copying static properties manually to work around Opera's bug, + // where "prototype" property is enumerable and overrides existing prototype + for (var prop in fabric.StaticCanvas) { + if (prop !== 'prototype') { + fabric.Canvas[prop] = fabric.StaticCanvas[prop]; + } + } + + if (fabric.isTouchSupported) { + /** @ignore */ + fabric.Canvas.prototype._setCursorFromEvent = function() { }; + } + + /** + * @class fabric.Element + * @alias fabric.Canvas + * @deprecated Use {@link fabric.Canvas} instead. + * @constructor + */ + fabric.Element = fabric.Canvas; +})(); + + +(function(){ + + var cursorMap = [ + 'n-resize', + 'ne-resize', + 'e-resize', + 'se-resize', + 's-resize', + 'sw-resize', + 'w-resize', + 'nw-resize' + ], + cursorOffset = { + 'mt': 0, // n + 'tr': 1, // ne + 'mr': 2, // e + 'br': 3, // se + 'mb': 4, // s + 'bl': 5, // sw + 'ml': 6, // w + 'tl': 7 // nw + }, + addListener = fabric.util.addListener, + removeListener = fabric.util.removeListener, + getPointer = fabric.util.getPointer; + + fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ { + + /** + * Adds mouse listeners to canvas + * @private + */ + _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, 'mousewheel', this._onMouseWheel); + + // touch events + addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); + addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); + + if (typeof Event !== 'undefined' && 'add' in Event) { + Event.add(this.upperCanvasEl, 'gesture', this._onGesture); + Event.add(this.upperCanvasEl, 'drag', this._onDrag); + Event.add(this.upperCanvasEl, 'orientation', this._onOrientationChange); + Event.add(this.upperCanvasEl, 'shake', this._onShake); + } + }, + + /** + * @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._onOrientationChange = this._onOrientationChange.bind(this); + this._onMouseWheel = this._onMouseWheel.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, 'mousewheel', this._onMouseWheel); + + removeListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); + removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); + + if (typeof Event !== 'undefined' && 'remove' in Event) { + Event.remove(this.upperCanvasEl, 'gesture', this._onGesture); + Event.remove(this.upperCanvasEl, 'drag', this._onDrag); + Event.remove(this.upperCanvasEl, 'orientation', this._onOrientationChange); + Event.remove(this.upperCanvasEl, 'shake', this._onShake); + } + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js gesture + * @param {Event} [self] Inner Event object + */ + _onGesture: function(e, s) { + this.__onTransformGesture && this.__onTransformGesture(e, s); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js drag + * @param {Event} [self] Inner Event object + */ + _onDrag: function(e, s) { + this.__onDrag && this.__onDrag(e, s); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js wheel event + * @param {Event} [self] Inner Event object + */ + _onMouseWheel: function(e, s) { + this.__onMouseWheel && this.__onMouseWheel(e, s); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js orientation change + * @param {Event} [self] Inner Event object + */ + _onOrientationChange: function(e,s) { + this.__onOrientationChange && this.__onOrientationChange(e,s); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js shake + * @param {Event} [self] Inner Event object + */ + _onShake: function(e,s) { + this.__onShake && this.__onShake(e,s); + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onMouseDown: function (e) { + this.__onMouseDown(e); + + addListener(fabric.document, 'mouseup', this._onMouseUp); + addListener(fabric.document, 'touchend', this._onMouseUp); + + addListener(fabric.document, 'mousemove', this._onMouseMove); + addListener(fabric.document, 'touchmove', this._onMouseMove); + + removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + removeListener(this.upperCanvasEl, 'touchmove', 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); + }, + + /** + * @private + * @param {Event} e Event object fired on mousemove + */ + _onMouseMove: function (e) { + !this.allowTouchScrolling && e.preventDefault && e.preventDefault(); + this.__onMouseMove(e); + }, + + /** + * @private + */ + _onResize: function () { + this.calcOffset(); + }, + + /** + * Decides whether the canvas should be redrawn in mouseup and mousedown events. + * @private + * @param {Object} target + * @param {Object} pointer + */ + _shouldRender: function(target, pointer) { + var activeObject = this.getActiveGroup() || this.getActiveObject(); + + return !!( + (target && ( + target.isMoving || + target !== activeObject)) + || + (!target && !!activeObject) + || + (!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; + + if (this.isDrawingMode && this._isCurrentlyDrawing) { + this._onMouseUpInDrawingMode(e); + return; + } + + if (this._currentTransform) { + this._finalizeCurrentTransform(); + target = this._currentTransform.target; + } + else { + target = this.findTarget(e, true); + } + + var shouldRender = this._shouldRender(target, this.getPointer(e)); + + this._maybeGroupObjects(e); + + if (target) { + target.isMoving = false; + } + + shouldRender && this.renderAll(); + + this._handleCursorAndEvent(e, target); + }, + + _handleCursorAndEvent: function(e, target) { + this._setCursorFromEvent(e, target); + + // TODO: why are we doing this? + var _this = this; + setTimeout(function () { + _this._setCursorFromEvent(e, target); + }, 50); + + this.fire('mouse:up', { target: target, e: e }); + target && target.fire('mouseup', { e: e }); + }, + + /** + * @private + */ + _finalizeCurrentTransform: function() { + + var transform = this._currentTransform; + var target = transform.target; + + if (target._scaling) { + target._scaling = false; + } + + target.setCoords(); + + // only fire :modified event if target coordinates were changed during mousedown-mouseup + if (this.stateful && target.hasStateChanged()) { + this.fire('object:modified', { target: target }); + target.fire('modified'); + } + + this._restoreOriginXY(target); + }, + + /** + * @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); + } + this.freeDrawingBrush.onMouseDown(this.getPointer(e)); + this.fire('mouse:down', { e: e }); + }, + + /** + * @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.upperCanvasEl.style.cursor = this.freeDrawingCursor; + this.fire('mouse:move', { e: e }); + }, + + /** + * @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.fire('mouse:up', { e: e }); + }, + + /** + * Method that defines the actions when mouse is clic ked on canvas. + * The method inits the currentTransform parameters and renders all the + * canvas so the current image can be placed on the top canvas and the rest + * in on the container one. + * @private + * @param {Event} e Event object fired on mousedown + */ + __onMouseDown: function (e) { + + // accept only left clicks + var isLeftClick = 'which' in e ? e.which === 1 : e.button === 1; + if (!isLeftClick && !fabric.isTouchSupported) return; + + if (this.isDrawingMode) { + this._onMouseDownInDrawingMode(e); + return; + } + + // ignore if some object is being transformed at this moment + if (this._currentTransform) return; + + var target = this.findTarget(e), + pointer = this.getPointer(e); + + // save pointer for check in __onMouseUp event + 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 && target.selectable && !shouldGroup) { + this._beforeTransform(e, target); + this._setupCurrentTransform(e, target); + } + // we must renderAll so that active image is placed on the top canvas + shouldRender && this.renderAll(); + + this.fire('mouse:down', { target: target, e: e }); + target && target.fire('mousedown', { e: e }); + }, + + /** + * @private + */ + _beforeTransform: function(e, target) { + var corner; + + this.stateful && target.saveState(); + + // determine if it's a drag or rotate case + if ((corner = target._findTargetCorner(e, this._offset))) { + this.onBeforeScaleRotate(target); + } + + if (target !== this.getActiveGroup() && target !== this.getActiveObject()) { + this.deactivateAll(); + this.setActiveObject(target, e); + } + }, + + /** + * @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; + } + + var groupSelector = this._groupSelector; + + // We initially clicked in an empty area, so we draw a box for multiple selection + if (groupSelector) { + pointer = getPointer(e, this.upperCanvasEl); + + groupSelector.left = pointer.x - this._offset.left - groupSelector.ex; + groupSelector.top = pointer.y - this._offset.top - groupSelector.ey; + + this.renderTop(); + } + else if (!this._currentTransform) { + + target = this.findTarget(e); + + if (!target || target && !target.selectable) { + this.upperCanvasEl.style.cursor = this.defaultCursor; + } + else { + this._setCursorFromEvent(e, target); + } + } + else { + this._transformObject(e); + } + + this.fire('mouse:move', { target: target, e: e }); + target && target.fire('mousemove', { e: e }); + }, + + /** + * @private + * @param {Event} e Event fired on mousemove + */ + _transformObject: function(e) { + + var pointer = getPointer(e, this.upperCanvasEl), + transform = this._currentTransform; + + transform.reset = false, + transform.target.isMoving = true; + + this._beforeScaleTransform(e, transform); + this._performTransformAction(e, transform, pointer); + + this.renderAll(); + }, + + /** + * @private + */ + _performTransformAction: function(e, transform, pointer) { + var x = pointer.x, + y = pointer.y, + target = transform.target, + action = transform.action; + + if (action === 'rotate') { + this._rotateObject(x, y); + this._fire('rotating', target, e); + } + else if (action === 'scale') { + this._onScale(e, transform, x, y); + this._fire('scaling', target, e); + } + else if (action === 'scaleX') { + this._scaleObject(x, y, 'x'); + this._fire('scaling', target, e); + } + else if (action === 'scaleY') { + this._scaleObject(x, y, 'y'); + this._fire('scaling', target, e); + } + else { + this._translateObject(x, y); + this._fire('moving', target, e); + this._setCursor(this.moveCursor); + } + }, + + /** + * @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(e, 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(e); + transform.reset = true; + } + } + }, + + /** + * @private + */ + _onScale: function(e, transform, x, y) { + // rotate object only if shift key is not pressed + // and if it is not a group we are transforming + if ((e.shiftKey || this.uniScaleTransform) && !transform.target.get('lockUniScaling')) { + transform.currentAction = 'scale'; + this._scaleObject(x, y); + } + else { + // Switch from a normal resize to proportional + if (!transform.reset && transform.currentAction === 'scale') { + this._resetCurrentTransform(e, transform.target); + } + + transform.currentAction = 'scaleEqually'; + 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) { + var style = this.upperCanvasEl.style; + + if (!target || !target.selectable) { + style.cursor = this.defaultCursor; + return false; + } + else { + var activeGroup = this.getActiveGroup(); + // only show proper corner when group selection is not active + var corner = target._findTargetCorner + && (!activeGroup || !activeGroup.contains(target)) + && target._findTargetCorner(e, this._offset); + + if (!corner) { + style.cursor = target.hoverCursor || this.hoverCursor; + } + else { + this._setCornerCursor(corner, target); + } + } + return true; + }, + + /** + * @private + */ + _setCornerCursor: function(corner, target) { + var style = this.upperCanvasEl.style; + + if (corner in cursorOffset) { + style.cursor = this._getRotatedCornerCursor(corner, target); + } + else if (corner === 'mtr' && target.hasRotatingPoint) { + style.cursor = this.rotationCursor; + } + else { + style.cursor = this.defaultCursor; + return false; + } + }, + + /** + * @private + */ + _getRotatedCornerCursor: function(corner, target) { + var n = Math.round((target.getAngle() % 360) / 45); + + if (n < 0) { + n += 8; // full circle ahead + } + n += cursorOffset[corner]; + // normalize n to be from 0 to 7 + n %= 8; + + return 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.shiftKey && + (this.getActiveGroup() || (activeObject && activeObject !== target)) + && this.selection; + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _handleGrouping: function (e, target) { + + if (target === this.getActiveGroup()) { + + // if it's a group, find target again, this time skipping group + target = this.findTarget(e, true); + + // if even object is not found, bail out + if (!target || target.isType('group')) { + return; + } + } + if (this.getActiveGroup()) { + 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); + this._resetObjectTransform(activeGroup); + 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)); + return; + } + } + else { + activeGroup.addWithUpdate(target); + this._resetObjectTransform(activeGroup); + } + 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); + + this.setActiveGroup(group); + 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(); + + var isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target); + + var groupObjects = isActiveLower + ? [ this._activeObject, target ] + : [ target, this._activeObject ]; + + return new fabric.Group(groupObjects, { + originX: 'center', + originY: 'center' + }); + }, + + /** + * @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(), { + originX: 'center', + originY: 'center' + }); + this.setActiveGroup(group, e); + group.saveCoords(); + this.fire('selection:created', { target: group }); + 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; + } + }); + +})(); + + +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, + top: options.top, + width: options.width, + height: options.height + }; + + if (multiplier !== 1) { + return this.__toDataURLWithMultiplier(format, quality, cropping, multiplier); + } + else { + return this.__toDataURL(format, quality, cropping); + } + }, + + /** + * @private + */ + __toDataURL: function(format, quality, cropping) { + + this.renderAll(true); + + var canvasEl = this.upperCanvasEl || this.lowerCanvasEl; + var croppedCanvasEl = this.__getCroppedCanvas(canvasEl, cropping); + + // to avoid common confusion https://github.com/kangax/fabric.js/issues/806 + if (format === 'jpg') { + format = 'jpeg'; + } + + var data = (fabric.StaticCanvas.supports('toDataURLWithQuality')) + ? (croppedCanvasEl || canvasEl).toDataURL('image/' + format, quality) + : (croppedCanvasEl || canvasEl).toDataURL('image/' + format); + + this.contextTop && this.clearContext(this.contextTop); + this.renderAll(); + + if (croppedCanvasEl) { + croppedCanvasEl = null; + } + + return data; + }, + + /** + * @private + */ + __getCroppedCanvas: function(canvasEl, cropping) { + + var croppedCanvasEl, + croppedCtx; + + var shouldCrop = 'left' in cropping || + 'top' in cropping || + 'width' in cropping || + 'height' in cropping; + + if (shouldCrop) { + + croppedCanvasEl = fabric.util.createCanvasElement(); + croppedCtx = croppedCanvasEl.getContext('2d'); + + croppedCanvasEl.width = cropping.width || this.width; + croppedCanvasEl.height = cropping.height || this.height; + + croppedCtx.drawImage(canvasEl, -cropping.left || 0, -cropping.top || 0); + } + + return croppedCanvasEl; + }, + + /** + * @private + */ + __toDataURLWithMultiplier: function(format, quality, cropping, multiplier) { + + var origWidth = this.getWidth(), + origHeight = this.getHeight(), + scaledWidth = origWidth * multiplier, + scaledHeight = origHeight * multiplier, + activeObject = this.getActiveObject(), + activeGroup = this.getActiveGroup(), + + ctx = this.contextTop || this.contextContainer; + + this.setWidth(scaledWidth).setHeight(scaledHeight); + ctx.scale(multiplier, multiplier); + + if (cropping.left) { + cropping.left *= multiplier; + } + if (cropping.top) { + cropping.top *= multiplier; + } + if (cropping.width) { + cropping.width *= multiplier; + } + if (cropping.height) { + cropping.height *= multiplier; + } + + if (activeGroup) { + // not removing group due to complications with restoring it with correct state afterwords + this._tempRemoveBordersControlsFromGroup(activeGroup); + } + else if (activeObject && this.deactivateAll) { + this.deactivateAll(); + } + + this.renderAll(true); + + var data = this.__toDataURL(format, quality, cropping); + + // restoring width, height for `renderAll` to draw + // background properly (while context is scaled) + this.width = origWidth; + this.height = origHeight; + + ctx.scale(1 / multiplier, 1 / multiplier); + this.setWidth(origWidth).setHeight(origHeight); + + if (activeGroup) { + this._restoreBordersControlsOnGroup(activeGroup); + } + else if (activeObject && this.setActiveObject) { + this.setActiveObject(activeObject); + } + + this.contextTop && this.clearContext(this.contextTop); + this.renderAll(); + + return data; + }, + + /** + * Exports canvas element to a dataurl image (allowing to change image size via multiplier). + * @deprecated since 1.0.13 + * @param {String} format (png|jpeg) + * @param {Number} multiplier + * @param {Number} quality (0..1) + * @return {String} + */ + toDataURLWithMultiplier: function (format, multiplier, quality) { + return this.toDataURL({ + format: format, + multiplier: multiplier, + quality: quality + }); + }, + + /** + * @private + */ + _tempRemoveBordersControlsFromGroup: function(group) { + group.origHasControls = group.hasControls; + group.origBorderColor = group.borderColor; + + group.hasControls = true; + group.borderColor = 'rgba(0,0,0,0)'; + + group.forEachObject(function(o) { + o.origBorderColor = o.borderColor; + o.borderColor = 'rgba(0,0,0,0)'; + }); + }, + + /** + * @private + */ + _restoreBordersControlsOnGroup: function(group) { + group.hideControls = group.origHideControls; + group.borderColor = group.origBorderColor; + + group.forEachObject(function(o) { + o.borderColor = o.origBorderColor; + delete o.origBorderColor; + }); + } +}); + + +fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { + + /** + * Populates canvas with data from the specified dataless JSON. + * JSON format must conform to the one of {@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) + : json; + + this.clear(); + + var _this = this; + this._enlivenObjects(serialized.objects, function () { + _this._setBgOverlay(serialized, 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); + + 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; + return; + } + + if (property === 'backgroundImage' || property === 'overlayImage') { + fabric.Image.fromObject(value, function(img) { + _this[property] = img; + 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.length === 0) { + callback && callback(); + return; + } + + var renderOnAddRemove = this.renderOnAddRemove; + this.renderOnAddRemove = false; + + fabric.util.enlivenObjects(objects, function(enlivenedObjects) { + enlivenedObjects.forEach(function(obj, index) { + _this.insertAt(obj, index, true); + }); + + _this.renderOnAddRemove = renderOnAddRemove; + callback && callback(); + }, null, reviver); + }, + + /** + * @private + * @param {String} format + * @param {Function} callback + */ + _toDataURL: function (format, callback) { + this.clone(function (clone) { + callback(clone.toDataURL(format)); + }); + }, + + /** + * @private + * @param {String} format + * @param {Number} multiplier + * @param {Function} callback + */ + _toDataURLWithMultiplier: function (format, multiplier, callback) { + this.clone(function (clone) { + callback(clone.toDataURLWithMultiplier(format, multiplier)); + }); + }, + + /** + * Clones canvas instance + * @param {Object} [callback] Receives cloned instance as a first argument + * @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, + toFixed = fabric.util.toFixed, + capitalize = fabric.util.string.capitalize, + degreesToRadians = fabric.util.degreesToRadians, + supportsLineDash = fabric.StaticCanvas.supports('setLineDash'); + + if (fabric.Object) { + return; + } + + /** + * Root object class from which all 2d shape classes inherit from + * @class fabric.Object + * @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 modified + * @fires rotating + * @fires scaling + * @fires moving + * + * @fires mousedown + * @fires mouseup + */ + fabric.Object = fabric.util.createClass(/** @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} + */ + + /** + * Sets object's {@link fabric.Object#angle|angle} + * @method setAngle + * @memberOf fabric.Object.prototype + * @param {Number} value Angle value (in degrees) + * @return {fabric.Object} thisArg + * @chainable + */ + + /** + * 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.) + * @type String + * @default + */ + type: 'object', + + /** + * Horizontal origin of transformation of an object (one of "left", "right", "center") + * @type String + * @default + */ + originX: 'left', + + /** + * Vertical origin of transformation of an object (one of "top", "bottom", "center") + * @type String + * @default + */ + originY: 'top', + + /** + * Top position of an object. Note that by default it's relative to object center. You can change this by setting originY={top/center/bottom} + * @type Number + * @default + */ + top: 0, + + /** + * Left position of an object. Note that by default it's relative to object center. You can change this by setting originX={left/center/right} + * @type Number + * @default + */ + left: 0, + + /** + * Object width + * @type Number + * @default + */ + width: 0, + + /** + * Object height + * @type Number + * @default + */ + height: 0, + + /** + * Object scale factor (horizontal) + * @type Number + * @default + */ + scaleX: 1, + + /** + * Object scale factor (vertical) + * @type Number + * @default + */ + scaleY: 1, + + /** + * When true, an object is rendered as flipped horizontally + * @type Boolean + * @default + */ + flipX: false, + + /** + * When true, an object is rendered as flipped vertically + * @type Boolean + * @default + */ + flipY: false, + + /** + * Opacity of an object + * @type Number + * @default + */ + opacity: 1, + + /** + * Angle of rotation of an object (in degrees) + * @type Number + * @default + */ + angle: 0, + + /** + * Size of object's controlling corners (in pixels) + * @type Number + * @default + */ + cornerSize: 12, + + /** + * When true, object's controlling corners are rendered as transparent inside (i.e. stroke instead of fill) + * @type Boolean + * @default + */ + transparentCorners: true, + + /** + * Default cursor value used when hovering over this object on canvas + * @type String + * @default + */ + hoverCursor: null, + + /** + * Padding between object and its controlling borders (in pixels) + * @type Number + * @default + */ + padding: 0, + + /** + * Color of controlling borders of an object (when it's active) + * @type String + * @default + */ + borderColor: 'rgba(102,153,255,0.75)', + + /** + * Color of controlling corners of an object (when it's active) + * @type String + * @default + */ + cornerColor: 'rgba(102,153,255,0.5)', + + /** + * When true, this object will use center point as the origin of transformation + * when being 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 + * @type String + * @default + */ + fillRule: 'source-over', + + /** + * Background color of an object. Only works with text objects at the moment. + * @type String + * @default + */ + backgroundColor: '', + + /** + * 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, + + /** + * 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 shadow clipTo visible backgroundColor' + ).split(' '), + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + if (options) { + this.setOptions(options); + } + }, + + /** + * @private + */ + _initGradient: function(options) { + if (options.fill && options.fill.colorStops && !(options.fill instanceof fabric.Gradient)) { + this.set('fill', new fabric.Gradient(options.fill)); + } + }, + + /** + * @private + */ + _initPattern: function(options) { + if (options.fill && options.fill.source && !(options.fill instanceof fabric.Pattern)) { + this.set('fill', new fabric.Pattern(options.fill)); + } + if (options.stroke && options.stroke.source && !(options.stroke instanceof fabric.Pattern)) { + this.set('stroke', new fabric.Pattern(options.stroke)); + } + }, + + /** + * @private + */ + _initClipping: function(options) { + if (!options.clipTo || typeof options.clipTo !== 'string') return; + + var functionBody = fabric.util.getFunctionBody(options.clipTo); + if (typeof functionBody !== 'undefined') { + this.clipTo = new Function('ctx', functionBody); + } + }, + + /** + * Sets object's properties from options + * @param {Object} [options] Options object + */ + setOptions: function(options) { + for (var prop in options) { + this.set(prop, options[prop]); + } + this._initGradient(options); + this._initPattern(options); + this._initClipping(options); + }, + + /** + * Transforms context when rendering an object + * @param {CanvasRenderingContext2D} ctx Context + * @param {Boolean} fromLeft When true, context is transformed to object's top/left corner. This is used when rendering text on Node + */ + transform: function(ctx, fromLeft) { + ctx.globalAlpha = this.opacity; + + var center = fromLeft ? this._getLeftTopCoords() : this.getCenterPoint(); + ctx.translate(center.x, center.y); + ctx.rotate(degreesToRadians(this.angle)); + ctx.scale( + this.scaleX * (this.flipX ? -1 : 1), + this.scaleY * (this.flipY ? -1 : 1) + ); + }, + + /** + * Returns an object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toObject: function(propertiesToInclude) { + + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + + var object = { + type: this.type, + originX: this.originX, + originY: this.originY, + left: toFixed(this.left, NUM_FRACTION_DIGITS), + top: toFixed(this.top, NUM_FRACTION_DIGITS), + width: toFixed(this.width, NUM_FRACTION_DIGITS), + height: toFixed(this.height, NUM_FRACTION_DIGITS), + fill: (this.fill && this.fill.toObject) ? this.fill.toObject() : this.fill, + stroke: (this.stroke && this.stroke.toObject) ? this.stroke.toObject() : this.stroke, + strokeWidth: toFixed(this.strokeWidth, NUM_FRACTION_DIGITS), + strokeDashArray: this.strokeDashArray, + strokeLineCap: this.strokeLineCap, + strokeLineJoin: this.strokeLineJoin, + strokeMiterLimit: toFixed(this.strokeMiterLimit, NUM_FRACTION_DIGITS), + scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS), + scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS), + angle: toFixed(this.getAngle(), NUM_FRACTION_DIGITS), + flipX: this.flipX, + flipY: this.flipY, + opacity: toFixed(this.opacity, NUM_FRACTION_DIGITS), + shadow: (this.shadow && this.shadow.toObject) ? this.shadow.toObject() : this.shadow, + visible: this.visible, + clipTo: this.clipTo && String(this.clipTo), + backgroundColor: this.backgroundColor + }; + + if (!this.includeDefaultValues) { + object = this._removeDefaultValues(object); + } + + fabric.util.populateWithProperties(this, object, propertiesToInclude); + + return object; + }, + + /** + * Returns (dataless) object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toDatalessObject: function(propertiesToInclude) { + // will be overwritten by subclasses + return this.toObject(propertiesToInclude); + }, + + /** + * @private + * @param {Object} object + */ + _removeDefaultValues: function(object) { + var prototype = fabric.util.getKlass(object.type).prototype; + var stateProperties = prototype.stateProperties; + + stateProperties.forEach(function(prop) { + if (object[prop] === prototype[prop]) { + delete object[prop]; + } + }); + + return object; + }, + + /** + * Returns a string representation of an instance + * @return {String} + */ + toString: function() { + return "#"; + }, + + /** + * Basic getter + * @param {String} property Property name + * @return {Any} value of a property + */ + get: function(property) { + return this[property]; + }, + + /** + * Sets property to a given value. When changing position/dimension -related properties (left, top, scale, angle, etc.) `set` does not update position of object's borders/controls. If you need to update those, call `setCoords()`. + * @param {String|Object} key Property name or object (if object, iterate over the object properties) + * @param {Object|Function} value Property value (if function, the value is passed into it and its return value is used as a new one) + * @return {fabric.Object} thisArg + * @chainable + */ + set: function(key, value) { + if (typeof key === 'object') { + for (var prop in key) { + this._set(prop, key[prop]); + } + } + else { + if (typeof value === 'function' && key !== 'clipTo') { + this._set(key, value(this.get(key))); + } + else { + this._set(key, value); + } + } + return this; + }, + + /** + * @private + * @param {String} key + * @param {Any} value + * @return {fabric.Object} thisArg + */ + _set: function(key, value) { + var shouldConstrainValue = (key === 'scaleX' || key === 'scaleY'); + + if (shouldConstrainValue) { + value = this._constrainScale(value); + } + if (key === 'scaleX' && value < 0) { + this.flipX = !this.flipX; + value *= -1; + } + else if (key === 'scaleY' && value < 0) { + this.flipY = !this.flipY; + value *= -1; + } + else if (key === 'width' || key === 'height') { + this.minScaleLimit = toFixed(Math.min(0.1, 1/Math.max(this.width, this.height)), 2); + } + else if (key === 'shadow' && value && !(value instanceof fabric.Shadow)) { + value = new fabric.Shadow(value); + } + + this[key] = value; + + return this; + }, + + /** + * Toggles specified property from `true` to `false` or from `false` to `true` + * @param {String} property Property to toggle + * @return {fabric.Object} thisArg + * @chainable + */ + toggle: function(property) { + var value = this.get(property); + if (typeof value === 'boolean') { + this.set(property, !value); + } + return this; + }, + + /** + * Sets sourcePath of an object + * @param {String} value Value to set sourcePath to + * @return {fabric.Object} thisArg + * @chainable + */ + setSourcePath: function(value) { + this.sourcePath = value; + return this; + }, + + /** + * Renders an object on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Boolean} [noTransform] When true, context is not transformed + */ + render: function(ctx, noTransform) { + // do not render if width/height are zeros or object is not visible + if (this.width === 0 || this.height === 0 || !this.visible) return; + + ctx.save(); + + this._transform(ctx, noTransform); + this._setStrokeStyles(ctx); + this._setFillStyles(ctx); + + var m = this.transformMatrix; + if (m && this.group) { + ctx.translate(-this.group.width/2, -this.group.height/2); + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + + this._setShadow(ctx); + this.clipTo && fabric.util.clipContext(this, ctx); + this._render(ctx, noTransform); + this.clipTo && ctx.restore(); + this._removeShadow(ctx); + + if (this.active && !noTransform) { + this.drawBorders(ctx); + this.drawControls(ctx); + } + ctx.restore(); + }, + + _transform: function(ctx, noTransform) { + var m = this.transformMatrix; + if (m && !this.group) { + ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + if (!noTransform) { + this.transform(ctx); + } + }, + + _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.stroke; + } + }, + + _setFillStyles: function(ctx) { + if (this.fill) { + ctx.fillStyle = this.fill.toLive + ? this.fill.toLive(ctx) + : this.fill; + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _setShadow: function(ctx) { + if (!this.shadow) return; + + ctx.shadowColor = this.shadow.color; + ctx.shadowBlur = this.shadow.blur; + ctx.shadowOffsetX = this.shadow.offsetX; + ctx.shadowOffsetY = this.shadow.offsetY; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _removeShadow: function(ctx) { + if (!this.shadow) return; + + ctx.shadowColor = ''; + ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderFill: function(ctx) { + if (!this.fill) return; + + if (this.fill.toLive) { + ctx.save(); + ctx.translate( + -this.width / 2 + this.fill.offsetX || 0, + -this.height / 2 + this.fill.offsetY || 0); + } + ctx.fill(); + if (this.fill.toLive) { + ctx.restore(); + } + if (this.shadow && !this.shadow.affectStroke) { + this._removeShadow(ctx); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderStroke: function(ctx) { + if (!this.stroke) return; + + ctx.save(); + if (this.strokeDashArray) { + // Spec requires the concatenation of two copies the dash list when the number of elements is odd + if (1 & this.strokeDashArray.length) { + this.strokeDashArray.push.apply(this.strokeDashArray, this.strokeDashArray); + } + + if (supportsLineDash) { + ctx.setLineDash(this.strokeDashArray); + this._stroke && this._stroke(ctx); + } + else { + this._renderDashedStroke && this._renderDashedStroke(ctx); + } + ctx.stroke(); + } + else { + this._stroke ? this._stroke(ctx) : ctx.stroke(); + } + this._removeShadow(ctx); + ctx.restore(); + }, + + /** + * Clones an instance + * @param {Function} callback Callback is invoked with a clone as a first argument + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {fabric.Object} clone of an instance + */ + clone: function(callback, propertiesToInclude) { + if (this.constructor.fromObject) { + return this.constructor.fromObject(this.toObject(propertiesToInclude), callback); + } + return new fabric.Object(this.toObject(propertiesToInclude)); + }, + + /** + * Creates an instance of fabric.Image out of an object + * @param callback {Function} callback, invoked with an instance as a first argument + * @return {fabric.Object} thisArg + */ + cloneAsImage: function(callback) { + var dataUrl = this.toDataURL(); + fabric.util.loadImage(dataUrl, function(img) { + if (callback) { + callback(new fabric.Image(img)); + } + }); + return this; + }, + + /** + * Converts an object into a data-url-like string + * @param {Object} options Options object + * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" + * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. + * @param {Number} [options.multiplier=1] Multiplier to scale by + * @param {Number} [options.left] Cropping left offset. 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 + */ + toDataURL: function(options) { + options || (options = { }); + + var el = fabric.util.createCanvasElement(), + boundingRect = this.getBoundingRect(); + + el.width = boundingRect.width; + el.height = boundingRect.height; + + fabric.util.wrapElement(el, 'div'); + var canvas = new fabric.Canvas(el); + + // to avoid common confusion https://github.com/kangax/fabric.js/issues/806 + if (options.format === 'jpg') { + options.format = 'jpeg'; + } + + if (options.format === 'jpeg') { + canvas.backgroundColor = '#fff'; + } + + var origParams = { + active: this.get('active'), + left: this.getLeft(), + top: this.getTop() + }; + + this.set('active', false); + this.setPositionByOrigin(new fabric.Point(el.width / 2, el.height / 2), 'center', 'center'); + + 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 type {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 + */ + complexity: function() { + return 0; + }, + + /** + * Returns a JSON representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} JSON + */ + toJSON: function(propertiesToInclude) { + // delegate, not alias + return this.toObject(propertiesToInclude); + }, + + /** + * Sets gradient (fill or stroke) of an object + * Backwards incompatibility note: This method was named "setGradientFill" until v1.1.0 + * @param {String} property Property name 'stroke' or 'fill' + * @param {Object} [options] Options object + * @param {String} [options.type] Type of gradient 'radial' or 'linear' + * @param {Number} [options.x1=0] x-coordinate of start point + * @param {Number} [options.y1=0] y-coordinate of start point + * @param {Number} [options.x2=0] x-coordinate of end point + * @param {Number} [options.y2=0] y-coordinate of end point + * @param {Number} [options.r1=0] Radius of start point (only for radial gradients) + * @param {Number} [options.r2=0] Radius of end point (only for radial gradients) + * @param {Object} [options.colorStops] Color stops object eg. {0: 'ff0000', 1: '000000'} + * @return {fabric.Object} thisArg + * @chainable + * @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; + } + + for (var position in options.colorStops) { + var color = new fabric.Color(options.colorStops[position]); + gradient.colorStops.push({offset: position, color: color.toRgb(), opacity: color.getAlpha()}); + } + + return this.set(property, fabric.Gradient.forObject(this, gradient)); + }, + + /** + * Sets pattern fill of an object + * @param {Object} options Options object + * @param {(String|HTMLImageElement)} options.source Pattern source + * @param {String} [options.repeat=repeat] Repeat property of a pattern (one of repeat, repeat-x, repeat-y or no-repeat) + * @param {Number} [options.offsetX=0] Pattern horizontal offset from object's left/top corner + * @param {Number} [options.offsetY=0] Pattern vertical offset from object's left/top corner + * @return {fabric.Object} thisArg + * @chainable + * @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', new fabric.Shadow(options)); + }, + + /** + * Sets "color" of an instance (alias of `set('fill', …)`) + * @param {String} color Color value + * @return {fabric.Text} thisArg + * @chainable + */ + setColor: function(color) { + this.set('fill', color); + return this; + }, + + /** + * Centers object horizontally on canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + centerH: function () { + this.canvas.centerObjectH(this); + return this; + }, + + /** + * Centers object vertically on canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + centerV: function () { + this.canvas.centerObjectV(this); + return this; + }, + + /** + * Centers object vertically and horizontally on canvas to which is was added last + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + center: function () { + this.canvas.centerObject(this); + return this; + }, + + /** + * Removes object from canvas to which it was added last + * @return {fabric.Object} thisArg + * @chainable + */ + remove: function() { + 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 objectLeftTop = this.translateToOriginPoint(this.getCenterPoint(), 'left', 'top'); + return { + x: pointer.x - objectLeftTop.x, + y: pointer.y - objectLeftTop.y + }; + } + }); + + 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; + + /** + * 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; + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Translates the coordinates from origin to center coordinates (based on the object's dimensions) + * @param {fabric.Point} point The point which corresponds to the originX and originY params + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + translateToCenterPoint: function(point, originX, originY) { + var cx = point.x, + cy = point.y, + strokeWidth = this.stroke ? this.strokeWidth : 0; + + if (originX === "left") { + cx = point.x + (this.getWidth() + strokeWidth * this.scaleX) / 2; + } + else if (originX === "right") { + cx = point.x - (this.getWidth() + strokeWidth * this.scaleX) / 2; + } + + if (originY === "top") { + cy = point.y + (this.getHeight() + strokeWidth * this.scaleY) / 2; + } + else if (originY === "bottom") { + cy = point.y - (this.getHeight() + strokeWidth * this.scaleY) / 2; + } + + // Apply the reverse rotation to the point (it's already scaled properly) + return fabric.util.rotatePoint(new fabric.Point(cx, cy), point, degreesToRadians(this.angle)); + }, + + /** + * Translates the coordinates from center to origin coordinates (based on the object's dimensions) + * @param {fabric.Point} point The point which corresponds to center of the object + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + translateToOriginPoint: function(center, originX, originY) { + var x = center.x, + y = center.y, + strokeWidth = this.stroke ? this.strokeWidth : 0; + + // Get the point coordinates + if (originX === "left") { + x = center.x - (this.getWidth() + strokeWidth * this.scaleX) / 2; + } + else if (originX === "right") { + x = center.x + (this.getWidth() + strokeWidth * this.scaleX) / 2; + } + if (originY === "top") { + y = center.y - (this.getHeight() + strokeWidth * this.scaleY) / 2; + } + else if (originY === "bottom") { + y = center.y + (this.getHeight() + strokeWidth * this.scaleY) / 2; + } + + // Apply the rotation to the point (it's already scaled properly) + return fabric.util.rotatePoint(new fabric.Point(x, y), center, degreesToRadians(this.angle)); + }, + + /** + * Returns the real center coordinates of the object + * @return {fabric.Point} + */ + getCenterPoint: function() { + var leftTop = new fabric.Point(this.left, this.top); + return this.translateToCenterPoint(leftTop, this.originX, this.originY); + }, + + /** + * Returns the coordinates of the object based on center coordinates + * @param {fabric.Point} point The point which corresponds to the originX and originY params + * @return {fabric.Point} + */ + // getOriginPoint: function(center) { + // return this.translateToOriginPoint(center, this.originX, this.originY); + // }, + + /** + * Returns the coordinates of the object as if it has a different origin + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + getPointByOrigin: function(originX, originY) { + var center = this.getCenterPoint(); + return this.translateToOriginPoint(center, originX, originY); + }, + + /** + * Returns the point in local coordinates + * @param {fabric.Point} 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(), + strokeWidth = this.stroke ? this.strokeWidth : 0, + x, y; + + if (originX && originY) { + if (originX === "left") { + x = center.x - (this.getWidth() + strokeWidth * this.scaleX) / 2; + } + else if (originX === "right") { + x = center.x + (this.getWidth() + strokeWidth * this.scaleX) / 2; + } + else { + x = center.x; + } + + if (originY === "top") { + y = center.y - (this.getHeight() + strokeWidth * this.scaleY) / 2; + } + else if (originY === "bottom") { + y = center.y + (this.getHeight() + strokeWidth * this.scaleY) / 2; + } + else { + y = center.y; + } + } + else { + x = this.left; + y = this.top; + } + + return fabric.util.rotatePoint(new fabric.Point(point.x, point.y), center, -degreesToRadians(this.angle)) + .subtractEquals(new fabric.Point(x, y)); + }, + + /** + * Returns the point in global coordinates + * @param {fabric.Point} The point relative to the local coordinate system + * @return {fabric.Point} + */ + // toGlobalPoint: function(point) { + // return fabric.util.rotatePoint(point, this.getCenterPoint(), degreesToRadians(this.angle)).addEquals(new fabric.Point(this.left, this.top)); + // }, + + /** + * Sets the position of the object taking into consideration the object's origin + * @param {fabric.Point} point The new position of the object + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {void} + */ + setPositionByOrigin: function(pos, originX, originY) { + var center = this.translateToCenterPoint(pos, originX, originY); + var position = this.translateToOriginPoint(center, this.originX, this.originY); + + this.set('left', position.x); + this.set('top', position.y); + }, + + /** + * @param {String} to One of 'left', 'center', 'right' + */ + adjustPosition: function(to) { + var angle = degreesToRadians(this.angle); + var hypotHalf = this.getWidth() / 2; + var xHalf = Math.cos(angle) * hypotHalf; + var yHalf = Math.sin(angle) * hypotHalf; + var hypotFull = this.getWidth(); + var xFull = Math.cos(angle) * hypotFull; + var yFull = Math.sin(angle) * hypotFull; + + if (this.originX === 'center' && to === 'left' || + this.originX === 'right' && to === 'center') { + // move half left + this.left -= xHalf; + this.top -= yHalf; + } + else if (this.originX === 'left' && to === 'center' || + this.originX === 'center' && to === 'right') { + // move half right + this.left += xHalf; + this.top += yHalf; + } + else if (this.originX === 'left' && to === 'right') { + // move full right + this.left += xFull; + this.top += yFull; + } + else if (this.originX === 'right' && to === 'left') { + // move full left + this.left -= xFull; + this.top -= yFull; + } + + this.setCoords(); + this.originX = to; + }, + + /** + * @private + */ + _getLeftTopCoords: function() { + return this.translateToOriginPoint(this.getCenterPoint(), 'left', 'center'); + } + }); + +})(); + + +(function() { + + var degreesToRadians = fabric.util.degreesToRadians; + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Object containing coordinates of object's controls + * @type Object + * @default + */ + oCoords: null, + + /** + * Checks if object intersects with an area formed by 2 points + * @param {Object} pointTL top-left point of area + * @param {Object} pointBR bottom-right point of area + * @return {Boolean} true if object intersects with an area formed by 2 points + */ + intersectsWithRect: function(pointTL, pointBR) { + var oCoords = this.oCoords, + tl = new fabric.Point(oCoords.tl.x, oCoords.tl.y), + tr = new fabric.Point(oCoords.tr.x, oCoords.tr.y), + bl = new fabric.Point(oCoords.bl.x, oCoords.bl.y), + br = new fabric.Point(oCoords.br.x, oCoords.br.y); + + var intersection = fabric.Intersection.intersectPolygonRectangle( + [tl, tr, br, bl], + pointTL, + pointBR + ); + return intersection.status === 'Intersection'; + }, + + /** + * Checks if object intersects with another object + * @param {Object} other Object to test + * @return {Boolean} true if object intersects with another object + */ + intersectsWithObject: function(other) { + // extracts coords + function getCoords(oCoords) { + return { + tl: new fabric.Point(oCoords.tl.x, oCoords.tl.y), + tr: new fabric.Point(oCoords.tr.x, oCoords.tr.y), + bl: new fabric.Point(oCoords.bl.x, oCoords.bl.y), + br: new fabric.Point(oCoords.br.x, oCoords.br.y) + }; + } + var thisCoords = getCoords(this.oCoords), + otherCoords = getCoords(other.oCoords); + + var intersection = fabric.Intersection.intersectPolygonPolygon( + [thisCoords.tl, thisCoords.tr, thisCoords.br, thisCoords.bl], + [otherCoords.tl, otherCoords.tr, otherCoords.br, otherCoords.bl] + ); + + return intersection.status === 'Intersection'; + }, + + /** + * Checks if object is fully contained within area of another object + * @param {Object} other Object to test + * @return {Boolean} true if object is fully contained within area of another object + */ + isContainedWithinObject: function(other) { + var boundingRect = other.getBoundingRect(), + point1 = new fabric.Point(boundingRect.left, boundingRect.top), + point2 = new fabric.Point(boundingRect.left + boundingRect.width, boundingRect.top + boundingRect.height); + + return this.isContainedWithinRect(point1, point2); + }, + + /** + * Checks if object is fully contained within area formed by 2 points + * @param {Object} pointTL top-left point of area + * @param {Object} pointBR bottom-right point of area + * @return {Boolean} true if object is fully contained within area formed by 2 points + */ + isContainedWithinRect: function(pointTL, pointBR) { + var boundingRect = this.getBoundingRect(); + + return ( + boundingRect.left > pointTL.x && + boundingRect.left + boundingRect.width < pointBR.x && + boundingRect.top > pointTL.y && + boundingRect.top + boundingRect.height < pointBR.y + ); + }, + + /** + * Checks if point is inside the object + * @param {fabric.Point} point Point to check against + * @return {Boolean} true if point is inside the object + */ + containsPoint: function(point) { + var lines = this._getImageLines(this.oCoords), + xPoints = this._findCrossPoints(point, lines); + + // if xPoints is odd then point is inside the object + return (xPoints !== 0 && xPoints % 2 === 1); + }, + + /** + * Method that returns an object with the object edges in it, given the coordinates of the corners + * @private + * @param {Object} oCoords Coordinates of the object corners + */ + _getImageLines: function(oCoords) { + return { + topline: { + o: oCoords.tl, + d: oCoords.tr + }, + rightline: { + o: oCoords.tr, + d: oCoords.br + }, + bottomline: { + o: oCoords.br, + d: oCoords.bl + }, + leftline: { + o: oCoords.bl, + d: oCoords.tl + } + }; + }, + + /** + * Helper method to determine how many cross points are between the 4 object edges + * and the horizontal line determined by a point on canvas + * @private + * @param {fabric.Point} point Point to check + * @param {Object} oCoords Coordinates of the object being evaluated + */ + _findCrossPoints: function(point, oCoords) { + var b1, b2, a1, a2, xi, yi, + xcount = 0, + iLine; + + for (var lineKey in oCoords) { + iLine = oCoords[lineKey]; + // optimisation 1: line below point. no cross + if ((iLine.o.y < point.y) && (iLine.d.y < point.y)) { + continue; + } + // optimisation 2: line above point. no cross + if ((iLine.o.y >= point.y) && (iLine.d.y >= point.y)) { + continue; + } + // optimisation 3: vertical line case + if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= point.x)) { + xi = iLine.o.x; + yi = point.y; + } + // calculate the intersection point + else { + b1 = 0; + b2 = (iLine.d.y - iLine.o.y) / (iLine.d.x - iLine.o.x); + a1 = point.y- b1 * point.x; + a2 = iLine.o.y - b2 * iLine.o.x; + + xi = - (a1 - a2) / (b1 - b2); + yi = a1 + b1 * xi; + } + // dont count xi < point.x cases + if (xi >= point.x) { + xcount += 1; + } + // optimisation 4: specific for square images + if (xcount === 2) { + break; + } + } + return xcount; + }, + + /** + * Returns width of an object's bounding rectangle + * @deprecated since 1.0.4 + * @return {Number} width value + */ + getBoundingRectWidth: function() { + return this.getBoundingRect().width; + }, + + /** + * Returns height of an object's bounding rectangle + * @deprecated since 1.0.4 + * @return {Number} height value + */ + getBoundingRectHeight: function() { + return this.getBoundingRect().height; + }, + + /** + * Returns coordinates of object's bounding rectangle (left, top, width, height) + * @return {Object} Object with left, top, width, height properties + */ + getBoundingRect: function() { + this.oCoords || this.setCoords(); + + var xCoords = [this.oCoords.tl.x, this.oCoords.tr.x, this.oCoords.br.x, this.oCoords.bl.x]; + var minX = fabric.util.array.min(xCoords); + var maxX = fabric.util.array.max(xCoords); + var width = Math.abs(minX - maxX); + + var yCoords = [this.oCoords.tl.y, this.oCoords.tr.y, this.oCoords.br.y, this.oCoords.bl.y]; + var minY = fabric.util.array.min(yCoords); + var maxY = fabric.util.array.max(yCoords); + var height = Math.abs(minY - maxY); + + return { + left: minX, + top: minY, + width: width, + height: height + }; + }, + + /** + * Returns width of an object + * @return {Number} width value + */ + getWidth: function() { + return this.width * this.scaleX; + }, + + /** + * Returns height of an object + * @return {Number} height value + */ + getHeight: function() { + return this.height * this.scaleY; + }, + + /** + * Makes sure the scale is valid and modifies it if necessary + * @private + * @param {Number} value + * @return {Number} + */ + _constrainScale: function(value) { + if (Math.abs(value) < this.minScaleLimit) { + if (value < 0) + return -this.minScaleLimit; + else + return this.minScaleLimit; + } + + return value; + }, + + /** + * Scales an object (equally by x and y) + * @param value {Number} scale factor + * @return {fabric.Object} thisArg + * @chainable + */ + scale: function(value) { + value = this._constrainScale(value); + + if (value < 0) { + this.flipX = !this.flipX; + this.flipY = !this.flipY; + value *= -1; + } + + this.scaleX = value; + this.scaleY = value; + this.setCoords(); + return this; + }, + + /** + * Scales an object to a given width, with respect to bounding box (scaling by x/y equally) + * @param value {Number} new width value + * @return {fabric.Object} thisArg + * @chainable + */ + scaleToWidth: function(value) { + // adjust to bounding rect factor so that rotated shapes would fit as well + var boundingRectFactor = this.getBoundingRectWidth() / this.getWidth(); + return this.scale(value / this.width / boundingRectFactor); + }, + + /** + * Scales an object to a given height, with respect to bounding box (scaling by x/y equally) + * @param value {Number} new height value + * @return {fabric.Object} thisArg + * @chainable + */ + scaleToHeight: function(value) { + // adjust to bounding rect factor so that rotated shapes would fit as well + var boundingRectFactor = this.getBoundingRectHeight() / this.getHeight(); + return this.scale(value / this.height / boundingRectFactor); + }, + + /** + * Sets corner position coordinates based on current angle, width and height + * @return {fabric.Object} thisArg + * @chainable + */ + setCoords: function() { + + var strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0, + padding = this.padding, + theta = degreesToRadians(this.angle); + + this.currentWidth = (this.width + strokeWidth) * this.scaleX + padding * 2; + this.currentHeight = (this.height + strokeWidth) * this.scaleY + padding * 2; + + // If width is negative, make postive. Fixes path selection issue + if (this.currentWidth < 0) { + this.currentWidth = Math.abs(this.currentWidth); + } + + var _hypotenuse = Math.sqrt( + Math.pow(this.currentWidth / 2, 2) + + Math.pow(this.currentHeight / 2, 2)); + + var _angle = Math.atan(isFinite(this.currentHeight / this.currentWidth) ? this.currentHeight / this.currentWidth : 0); + + // offset added for rotate and scale actions + var offsetX = Math.cos(_angle + theta) * _hypotenuse, + offsetY = Math.sin(_angle + theta) * _hypotenuse, + sinTh = Math.sin(theta), + cosTh = Math.cos(theta); + + var coords = this.getCenterPoint(); + var tl = { + x: coords.x - offsetX, + y: coords.y - offsetY + }; + var tr = { + x: tl.x + (this.currentWidth * cosTh), + y: tl.y + (this.currentWidth * sinTh) + }; + var br = { + x: tr.x - (this.currentHeight * sinTh), + y: tr.y + (this.currentHeight * cosTh) + }; + var bl = { + x: tl.x - (this.currentHeight * sinTh), + y: tl.y + (this.currentHeight * cosTh) + }; + var ml = { + x: tl.x - (this.currentHeight/2 * sinTh), + y: tl.y + (this.currentHeight/2 * cosTh) + }; + var mt = { + x: tl.x + (this.currentWidth/2 * cosTh), + y: tl.y + (this.currentWidth/2 * sinTh) + }; + var mr = { + x: tr.x - (this.currentHeight/2 * sinTh), + y: tr.y + (this.currentHeight/2 * cosTh) + }; + var mb = { + x: bl.x + (this.currentWidth/2 * cosTh), + y: bl.y + (this.currentWidth/2 * sinTh) + }; + var mtr = { + x: mt.x, + y: mt.y + }; + + // debugging + + // setTimeout(function() { + // canvas.contextTop.fillStyle = 'green'; + // canvas.contextTop.fillRect(mb.x, mb.y, 3, 3); + // canvas.contextTop.fillRect(bl.x, bl.y, 3, 3); + // canvas.contextTop.fillRect(br.x, br.y, 3, 3); + // canvas.contextTop.fillRect(tl.x, tl.y, 3, 3); + // canvas.contextTop.fillRect(tr.x, tr.y, 3, 3); + // canvas.contextTop.fillRect(ml.x, ml.y, 3, 3); + // canvas.contextTop.fillRect(mr.x, mr.y, 3, 3); + // canvas.contextTop.fillRect(mt.x, mt.y, 3, 3); + // }, 50); + + this.oCoords = { + // corners + tl: tl, tr: tr, br: br, bl: bl, + // middle + ml: ml, mt: mt, mr: mr, mb: mb, + // rotating point + mtr: mtr + }; + + // set coordinates of the draggable boxes in the corners used to scale/rotate the image + this._setCornerCoords && this._setCornerCoords(); + + return this; + } + }); +})(); + + +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_ */ +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Returns styles-string for svg-export + * @return {String} + */ + getSvgStyles: function() { + + var fill = this.fill + ? (this.fill.toLive ? 'url(#SVGID_' + this.fill.id + ')' : this.fill) + : 'none'; + + var stroke = this.stroke + ? (this.stroke.toLive ? 'url(#SVGID_' + this.stroke.id + ')' : this.stroke) + : 'none'; + + var strokeWidth = this.strokeWidth ? this.strokeWidth : '0'; + var strokeDashArray = this.strokeDashArray ? this.strokeDashArray.join(' ') : ''; + var strokeLineCap = this.strokeLineCap ? this.strokeLineCap : 'butt'; + var strokeLineJoin = this.strokeLineJoin ? this.strokeLineJoin : 'miter'; + var strokeMiterLimit = this.strokeMiterLimit ? this.strokeMiterLimit : '4'; + var opacity = typeof this.opacity !== 'undefined' ? this.opacity : '1'; + + var visibility = this.visible ? '' : " visibility: hidden;"; + var filter = this.shadow && this.type !== 'text' ? 'filter: url(#SVGID_' + this.shadow.id + ');' : ''; + + return [ + "stroke: ", stroke, "; ", + "stroke-width: ", strokeWidth, "; ", + "stroke-dasharray: ", strokeDashArray, "; ", + "stroke-linecap: ", strokeLineCap, "; ", + "stroke-linejoin: ", strokeLineJoin, "; ", + "stroke-miterlimit: ", strokeMiterLimit, "; ", + "fill: ", fill, "; ", + "opacity: ", opacity, ";", + filter, + visibility + ].join(''); + }, + + /** + * Returns transform-string for svg-export + * @return {String} + */ + getSvgTransform: function() { + var toFixed = fabric.util.toFixed; + var angle = this.getAngle(); + var center = this.getCenterPoint(); + + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + + var translatePart = "translate(" + + toFixed(center.x, NUM_FRACTION_DIGITS) + + " " + + toFixed(center.y, NUM_FRACTION_DIGITS) + + ")"; + + var anglePart = angle !== 0 + ? (" rotate(" + toFixed(angle, NUM_FRACTION_DIGITS) + ")") + : ''; + + var scalePart = (this.scaleX === 1 && this.scaleY === 1) + ? '' : + (" scale(" + + toFixed(this.scaleX, NUM_FRACTION_DIGITS) + + " " + + toFixed(this.scaleY, NUM_FRACTION_DIGITS) + + ")"); + + var flipXPart = this.flipX ? "matrix(-1 0 0 1 0 0) " : ""; + var flipYPart = this.flipY ? "matrix(1 0 0 -1 0 0)" : ""; + + return [ translatePart, anglePart, scalePart, flipXPart, flipYPart ].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_ */ + + +/* + Depends on `stateProperties` +*/ +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Returns true if object state (one of its state properties) was changed + * @return {Boolean} true if instance' state has changed since `{@link fabric.Object#saveState}` was called + */ + hasStateChanged: function() { + return this.stateProperties.some(function(prop) { + return this.get(prop) !== this.originalState[prop]; + }, this); + }, + + /** + * Saves state of an object + * @param {Object} [options] Object with additional `stateProperties` array to include when saving state + * @return {fabric.Object} thisArg + */ + saveState: function(options) { + this.stateProperties.forEach(function(prop) { + this.originalState[prop] = this.get(prop); + }, this); + + if (options && options.stateProperties) { + options.stateProperties.forEach(function(prop) { + this.originalState[prop] = this.get(prop); + }, this); + } + + return this; + }, + + /** + * Setups state of an object + * @return {fabric.Object} thisArg + */ + setupState: function() { + this.originalState = { }; + this.saveState(); + + return this; + } +}); + + +(function(){ + + var getPointer = fabric.util.getPointer, + degreesToRadians = fabric.util.degreesToRadians, + isVML = typeof G_vmlCanvasManager !== 'undefined'; + + 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 {Event} e Event object + * @param {Object} offset Canvas offset + * @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found + */ + _findTargetCorner: function(e, offset) { + if (!this.hasControls || !this.active) return false; + + var pointer = getPointer(e, this.canvas.upperCanvasEl), + ex = pointer.x - offset.left, + ey = pointer.y - offset.top, + xPoints, + lines; + + for (var i in this.oCoords) { + + if (!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, + theta = degreesToRadians(this.angle), + newTheta = degreesToRadians(45 - this.angle), + cornerHypotenuse = Math.sqrt(2 * Math.pow(this.cornerSize, 2)) / 2, + cosHalfOffset = cornerHypotenuse * Math.cos(newTheta), + sinHalfOffset = cornerHypotenuse * Math.sin(newTheta), + sinTh = Math.sin(theta), + cosTh = Math.cos(theta); + + coords.tl.corner = { + tl: { + x: coords.tl.x - sinHalfOffset, + y: coords.tl.y - cosHalfOffset + }, + tr: { + x: coords.tl.x + cosHalfOffset, + y: coords.tl.y - sinHalfOffset + }, + bl: { + x: coords.tl.x - cosHalfOffset, + y: coords.tl.y + sinHalfOffset + }, + br: { + x: coords.tl.x + sinHalfOffset, + y: coords.tl.y + cosHalfOffset + } + }; + + coords.tr.corner = { + tl: { + x: coords.tr.x - sinHalfOffset, + y: coords.tr.y - cosHalfOffset + }, + tr: { + x: coords.tr.x + cosHalfOffset, + y: coords.tr.y - sinHalfOffset + }, + br: { + x: coords.tr.x + sinHalfOffset, + y: coords.tr.y + cosHalfOffset + }, + bl: { + x: coords.tr.x - cosHalfOffset, + y: coords.tr.y + sinHalfOffset + } + }; + + coords.bl.corner = { + tl: { + x: coords.bl.x - sinHalfOffset, + y: coords.bl.y - cosHalfOffset + }, + bl: { + x: coords.bl.x - cosHalfOffset, + y: coords.bl.y + sinHalfOffset + }, + br: { + x: coords.bl.x + sinHalfOffset, + y: coords.bl.y + cosHalfOffset + }, + tr: { + x: coords.bl.x + cosHalfOffset, + y: coords.bl.y - sinHalfOffset + } + }; + + coords.br.corner = { + tr: { + x: coords.br.x + cosHalfOffset, + y: coords.br.y - sinHalfOffset + }, + bl: { + x: coords.br.x - cosHalfOffset, + y: coords.br.y + sinHalfOffset + }, + br: { + x: coords.br.x + sinHalfOffset, + y: coords.br.y + cosHalfOffset + }, + tl: { + x: coords.br.x - sinHalfOffset, + y: coords.br.y - cosHalfOffset + } + }; + + coords.ml.corner = { + tl: { + x: coords.ml.x - sinHalfOffset, + y: coords.ml.y - cosHalfOffset + }, + tr: { + x: coords.ml.x + cosHalfOffset, + y: coords.ml.y - sinHalfOffset + }, + bl: { + x: coords.ml.x - cosHalfOffset, + y: coords.ml.y + sinHalfOffset + }, + br: { + x: coords.ml.x + sinHalfOffset, + y: coords.ml.y + cosHalfOffset + } + }; + + coords.mt.corner = { + tl: { + x: coords.mt.x - sinHalfOffset, + y: coords.mt.y - cosHalfOffset + }, + tr: { + x: coords.mt.x + cosHalfOffset, + y: coords.mt.y - sinHalfOffset + }, + bl: { + x: coords.mt.x - cosHalfOffset, + y: coords.mt.y + sinHalfOffset + }, + br: { + x: coords.mt.x + sinHalfOffset, + y: coords.mt.y + cosHalfOffset + } + }; + + coords.mr.corner = { + tl: { + x: coords.mr.x - sinHalfOffset, + y: coords.mr.y - cosHalfOffset + }, + tr: { + x: coords.mr.x + cosHalfOffset, + y: coords.mr.y - sinHalfOffset + }, + bl: { + x: coords.mr.x - cosHalfOffset, + y: coords.mr.y + sinHalfOffset + }, + br: { + x: coords.mr.x + sinHalfOffset, + y: coords.mr.y + cosHalfOffset + } + }; + + coords.mb.corner = { + tl: { + x: coords.mb.x - sinHalfOffset, + y: coords.mb.y - cosHalfOffset + }, + tr: { + x: coords.mb.x + cosHalfOffset, + y: coords.mb.y - sinHalfOffset + }, + bl: { + x: coords.mb.x - cosHalfOffset, + y: coords.mb.y + sinHalfOffset + }, + br: { + x: coords.mb.x + sinHalfOffset, + y: coords.mb.y + cosHalfOffset + } + }; + + coords.mtr.corner = { + tl: { + x: coords.mtr.x - sinHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y - cosHalfOffset - (cosTh * this.rotatingPointOffset) + }, + tr: { + x: coords.mtr.x + cosHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y - sinHalfOffset - (cosTh * this.rotatingPointOffset) + }, + bl: { + x: coords.mtr.x - cosHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y + sinHalfOffset - (cosTh * this.rotatingPointOffset) + }, + br: { + x: coords.mtr.x + sinHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y + cosHalfOffset - (cosTh * this.rotatingPointOffset) + } + }; + }, + /** + * Draws borders of an object's bounding box. + * Requires public properties: width, height + * Requires public options: padding, borderColor + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @return {fabric.Object} thisArg + * @chainable + */ + drawBorders: function(ctx) { + if (!this.hasBorders) return this; + + var padding = this.padding, + padding2 = padding * 2, + strokeWidth = ~~(this.strokeWidth / 2) * 2; // Round down to even number + + ctx.save(); + + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + ctx.strokeStyle = this.borderColor; + + var scaleX = 1 / this._constrainScale(this.scaleX), + scaleY = 1 / this._constrainScale(this.scaleY); + + ctx.lineWidth = 1 / this.borderScaleFactor; + + ctx.scale(scaleX, scaleY); + + var w = this.getWidth(), + h = this.getHeight(); + + ctx.strokeRect( + ~~(-(w / 2) - padding - strokeWidth / 2 * this.scaleX) - 0.5, // offset needed to make lines look sharper + ~~(-(h / 2) - padding - strokeWidth / 2 * this.scaleY) - 0.5, + ~~(w + padding2 + strokeWidth * this.scaleX) + 1, // double offset needed to make lines look sharper + ~~(h + padding2 + strokeWidth * this.scaleY) + 1 + ); + + if (this.hasRotatingPoint && this.isControlVisible('mtr') && !this.get('lockRotation') && this.hasControls) { + + var rotateHeight = ( + this.flipY + ? h + (strokeWidth * this.scaleY) + (padding * 2) + : -h - (strokeWidth * this.scaleY) - (padding * 2) + ) / 2; + + ctx.beginPath(); + ctx.moveTo(0, rotateHeight); + ctx.lineTo(0, rotateHeight + (this.flipY ? this.rotatingPointOffset : -this.rotatingPointOffset)); + ctx.closePath(); + ctx.stroke(); + } + + ctx.restore(); + return this; + }, + + /** + * Draws corners of an object's bounding box. + * Requires public properties: width, height, scaleX, scaleY + * Requires public options: cornerSize, padding + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @return {fabric.Object} thisArg + * @chainable + */ + drawControls: function(ctx) { + if (!this.hasControls) return this; + + var size = this.cornerSize, + size2 = size / 2, + strokeWidth2 = ~~(this.strokeWidth / 2), // half strokeWidth rounded down + left = -(this.width / 2), + top = -(this.height / 2), + paddingX = this.padding / this.scaleX, + paddingY = this.padding / this.scaleY, + scaleOffsetY = size2 / this.scaleY, + scaleOffsetX = size2 / this.scaleX, + scaleOffsetSizeX = (size2 - size) / this.scaleX, + scaleOffsetSizeY = (size2 - size) / this.scaleY, + height = this.height, + width = this.width, + methodName = this.transparentCorners ? 'strokeRect' : 'fillRect'; + + ctx.save(); + + ctx.lineWidth = 1 / Math.max(this.scaleX, this.scaleY); + + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + ctx.strokeStyle = ctx.fillStyle = this.cornerColor; + + // top-left + this._drawControl('tl', ctx, methodName, + left - scaleOffsetX - strokeWidth2 - paddingX, + top - scaleOffsetY - strokeWidth2 - paddingY); + + // top-right + this._drawControl('tr', ctx, methodName, + left + width - scaleOffsetX + strokeWidth2 + paddingX, + top - scaleOffsetY - strokeWidth2 - paddingY); + + // bottom-left + this._drawControl('bl', ctx, methodName, + left - scaleOffsetX - strokeWidth2 - paddingX, + top + height + scaleOffsetSizeY + strokeWidth2 + paddingY); + + // bottom-right + this._drawControl('br', ctx, methodName, + left + width + scaleOffsetSizeX + strokeWidth2 + paddingX, + top + height + scaleOffsetSizeY + strokeWidth2 + paddingY); + + if (!this.get('lockUniScaling')) { + + // middle-top + this._drawControl('mt', ctx, methodName, + left + width/2 - scaleOffsetX, + top - scaleOffsetY - strokeWidth2 - paddingY); + + // middle-bottom + this._drawControl('mb', ctx, methodName, + left + width/2 - scaleOffsetX, + top + height + scaleOffsetSizeY + strokeWidth2 + paddingY); + + // middle-right + this._drawControl('mb', ctx, methodName, + left + width + scaleOffsetSizeX + strokeWidth2 + paddingX, + top + height/2 - scaleOffsetY); + + // middle-left + this._drawControl('ml', ctx, methodName, + left - scaleOffsetX - strokeWidth2 - paddingX, + top + height/2 - scaleOffsetY); + } + + // middle-top-rotate + if (this.hasRotatingPoint) { + this._drawControl('mtr', ctx, methodName, + left + width/2 - scaleOffsetX, + this.flipY + ? (top + height + (this.rotatingPointOffset / this.scaleY) - this.cornerSize/this.scaleX/2 + strokeWidth2 + paddingY) + : (top - (this.rotatingPointOffset / this.scaleY) - this.cornerSize/this.scaleY/2 - strokeWidth2 - paddingY)); + } + + ctx.restore(); + + return this; + }, + + /** + * @private + */ + _drawControl: function(control, ctx, methodName, left, top) { + var sizeX = this.cornerSize / this.scaleX, + sizeY = this.cornerSize / this.scaleY; + + if (this.isControlVisible(control)) { + isVML || this.transparentCorners || ctx.clearRect(left, top, sizeX, sizeY); + ctx[methodName](left, top, sizeX, sizeY); + } + }, + + /** + * 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 to animate (if string) or properties to animate (if object) + * @param {Number|Object} value to animate property to (if string was given first) or options object + * @return {fabric.Object} thisArg + * @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' + ); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + /* _TO_SVG_END_ */ + + /** + * Returns complexity of an instance + * @return {Number} complexity + */ + complexity: function() { + return 1; + } + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Line.fromElement}) + * @static + * @memberOf fabric.Line + * @see http://www.w3.org/TR/SVG/shapes.html#LineElement + */ + fabric.Line.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('x1 y1 x2 y2'.split(' ')); + + /** + * Returns fabric.Line instance from an SVG element + * @static + * @memberOf fabric.Line + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @return {fabric.Line} instance of fabric.Line + */ + fabric.Line.fromElement = function(element, options) { + var parsedAttributes = fabric.parseAttributes(element, fabric.Line.ATTRIBUTE_NAMES); + var points = [ + parsedAttributes.x1 || 0, + parsedAttributes.y1 || 0, + parsedAttributes.x2 || 0, + parsedAttributes.y2 || 0 + ]; + return new fabric.Line(points, extend(parsedAttributes, options)); + }; + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Line instance from an object representation + * @static + * @memberOf fabric.Line + * @param {Object} object Object to create an instance from + * @return {fabric.Line} instance of fabric.Line + */ + fabric.Line.fromObject = function(object) { + var points = [object.x1, object.y1, object.x2, object.y2]; + return new fabric.Line(points, object); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + piBy2 = Math.PI * 2, + extend = fabric.util.object.extend; + + if (fabric.Circle) { + fabric.warn('fabric.Circle is already defined.'); + return; + } + + /** + * Circle class + * @class fabric.Circle + * @extends fabric.Object + * @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', + + /** + * Constructor + * @param {Object} [options] Options object + * @return {fabric.Circle} thisArg + */ + initialize: function(options) { + options = options || { }; + + this.set('radius', options.radius || 0); + this.callSuper('initialize', options); + }, + + /** + * @private + * @param {String} key + * @param {Any} value + * @return {fabric.Circle} thisArg + */ + _set: function(key, value) { + this.callSuper('_set', key, value); + + if (key === 'radius') { + this.setRadius(value); + } + + return this; + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] 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), { + radius: this.get('radius') + }); + }, + + /* _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( + '' + ); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + /* _TO_SVG_END_ */ + + /** + * @private + * @param ctx {CanvasRenderingContext2D} context to render on + */ + _render: function(ctx, noTransform) { + ctx.beginPath(); + // multiply by currently set alpha (the one that was set by path group where this object is contained, for example) + ctx.globalAlpha = this.group ? (ctx.globalAlpha * this.opacity) : this.opacity; + ctx.arc(noTransform ? this.left : 0, noTransform ? this.top : 0, this.radius, 0, piBy2, false); + ctx.closePath(); + + this._renderFill(ctx); + this.stroke && this._renderStroke(ctx); + }, + + /** + * Returns horizontal radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRadiusX: function() { + return this.get('radius') * this.get('scaleX'); + }, + + /** + * Returns vertical radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRadiusY: function() { + return this.get('radius') * this.get('scaleY'); + }, + + /** + * Sets radius of an object (and updates width accordingly) + * @return {Number} + */ + setRadius: function(value) { + this.radius = value; + this.set('width', value * 2).set('height', value * 2); + }, + + /** + * Returns complexity of an instance + * @return {Number} complexity of this instance + */ + complexity: function() { + return 1; + } + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Circle.fromElement}) + * @static + * @memberOf fabric.Circle + * @see: http://www.w3.org/TR/SVG/shapes.html#CircleElement + */ + fabric.Circle.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('cx cy r'.split(' ')); + + /** + * Returns {@link fabric.Circle} instance from an SVG element + * @static + * @memberOf fabric.Circle + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @throws {Error} If value of `r` attribute is missing or invalid + * @return {fabric.Circle} Instance of fabric.Circle + */ + fabric.Circle.fromElement = function(element, options) { + options || (options = { }); + var parsedAttributes = fabric.parseAttributes(element, fabric.Circle.ATTRIBUTE_NAMES); + if (!isValidRadius(parsedAttributes)) { + throw new Error('value of `r` attribute is required and can not be negative'); + } + if ('left' in parsedAttributes) { + parsedAttributes.left -= (options.width / 2) || 0; + } + if ('top' in parsedAttributes) { + parsedAttributes.top -= (options.height / 2) || 0; + } + var obj = new fabric.Circle(extend(parsedAttributes, options)); + + obj.cx = parseFloat(element.getAttribute('cx')) || 0; + obj.cy = parseFloat(element.getAttribute('cy')) || 0; + + return obj; + }; + + /** + * @private + */ + function isValidRadius(attributes) { + return (('radius' in attributes) && (attributes.radius > 0)); + } + /* _FROM_SVG_END_ */ + + /** + * Returns {@link fabric.Circle} instance from an object representation + * @static + * @memberOf fabric.Circle + * @param {Object} object Object to create an instance from + * @return {Object} Instance of fabric.Circle + */ + fabric.Circle.fromObject = function(object) { + return new fabric.Circle(object); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + "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) { + options = options || { }; + + this.callSuper('initialize', options); + + this.set('width', options.width || 100) + .set('height', options.height || 100); + }, + + /** + * @private + * @param ctx {CanvasRenderingContext2D} Context to render on + */ + _render: function(ctx) { + var widthBy2 = this.width / 2, + heightBy2 = this.height / 2; + + ctx.beginPath(); + ctx.moveTo(-widthBy2, heightBy2); + ctx.lineTo(0, -heightBy2); + ctx.lineTo(widthBy2, heightBy2); + ctx.closePath(); + + this._renderFill(ctx); + this._renderStroke(ctx); + }, + + /** + * @private + * @param ctx {CanvasRenderingContext2D} Context to render on + */ + _renderDashedStroke: function(ctx) { + var widthBy2 = this.width / 2, + heightBy2 = this.height / 2; + + ctx.beginPath(); + fabric.util.drawDashedLine(ctx, -widthBy2, heightBy2, 0, -heightBy2, this.strokeDashArray); + fabric.util.drawDashedLine(ctx, 0, -heightBy2, widthBy2, heightBy2, this.strokeDashArray); + fabric.util.drawDashedLine(ctx, widthBy2, heightBy2, -widthBy2, heightBy2, this.strokeDashArray); + ctx.closePath(); + }, + + /* _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; + + var points = [ + -widthBy2 + " " + heightBy2, + "0 " + -heightBy2, + widthBy2 + " " + heightBy2 + ].join(","); + + markup.push( + '' + ); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + /* _TO_SVG_END_ */ + + /** + * Returns complexity of an instance + * @return {Number} complexity of this instance + */ + complexity: function() { + return 1; + } + }); + + /** + * Returns fabric.Triangle instance from an object representation + * @static + * @memberOf fabric.Triangle + * @param object {Object} object to create an instance from + * @return {Object} instance of Canvas.Triangle + */ + fabric.Triangle.fromObject = function(object) { + return new fabric.Triangle(object); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global){ + + "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; + } + + /** + * 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, + + /** + * Constructor + * @param {Object} [options] Options object + * @return {fabric.Ellipse} thisArg + */ + initialize: function(options) { + options = options || { }; + + this.callSuper('initialize', options); + + this.set('rx', options.rx || 0); + this.set('ry', options.ry || 0); + + this.set('width', this.get('rx') * 2); + this.set('height', this.get('ry') * 2); + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] 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), { + rx: this.get('rx'), + ry: this.get('ry') + }); + }, + + /* _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( + '' + ); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + /* _TO_SVG_END_ */ + + /** + * Renders this instance on a given context + * @param ctx {CanvasRenderingContext2D} context to render on + * @param noTransform {Boolean} context is not transformed when set to true + */ + render: function(ctx, noTransform) { + // do not use `get` for perf. reasons + if (this.rx === 0 || this.ry === 0) return; + return this.callSuper('render', ctx, noTransform); + }, + + /** + * @private + * @param ctx {CanvasRenderingContext2D} context to render on + */ + _render: function(ctx, noTransform) { + ctx.beginPath(); + ctx.save(); + ctx.globalAlpha = this.group ? (ctx.globalAlpha * this.opacity) : this.opacity; + if (this.transformMatrix && this.group) { + ctx.translate(this.cx, this.cy); + } + ctx.transform(1, 0, 0, this.ry/this.rx, 0, 0); + ctx.arc(noTransform ? this.left : 0, noTransform ? this.top : 0, this.rx, 0, piBy2, false); + + this._renderFill(ctx); + this._renderStroke(ctx); + ctx.restore(); + }, + + /** + * Returns complexity of an instance + * @return {Number} complexity + */ + complexity: function() { + return 1; + } + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Ellipse.fromElement}) + * @static + * @memberOf fabric.Ellipse + * @see http://www.w3.org/TR/SVG/shapes.html#EllipseElement + */ + fabric.Ellipse.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('cx cy rx ry'.split(' ')); + + /** + * Returns {@link fabric.Ellipse} instance from an SVG element + * @static + * @memberOf fabric.Ellipse + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @return {fabric.Ellipse} + */ + fabric.Ellipse.fromElement = function(element, options) { + options || (options = { }); + + var parsedAttributes = fabric.parseAttributes(element, fabric.Ellipse.ATTRIBUTE_NAMES); + var cx = parsedAttributes.left; + var cy = parsedAttributes.top; + + if ('left' in parsedAttributes) { + parsedAttributes.left -= (options.width / 2) || 0; + } + if ('top' in parsedAttributes) { + parsedAttributes.top -= (options.height / 2) || 0; + } + + var ellipse = new fabric.Ellipse(extend(parsedAttributes, options)); + + ellipse.cx = cx || 0; + ellipse.cy = cy || 0; + + return ellipse; + }; + /* _FROM_SVG_END_ */ + + /** + * Returns {@link fabric.Ellipse} instance from an object representation + * @static + * @memberOf fabric.Ellipse + * @param {Object} object Object to create an instance from + * @return {fabric.Ellipse} + */ + fabric.Ellipse.fromObject = function(object) { + return new fabric.Ellipse(object); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend; + + if (fabric.Rect) { + console.warn('fabric.Rect is already defined'); + return; + } + + var stateProperties = fabric.Object.prototype.stateProperties.concat(); + stateProperties.push('rx', 'ry', 'x', 'y'); + + /** + * 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, + + /** + * @type Number + * @default + */ + x: 0, + + /** + * @type Number + * @default + */ + y: 0, + + /** + * Used to specify dash pattern for stroke on this object + * @type Array + */ + strokeDashArray: null, + + /** + * Constructor + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(options) { + options = options || { }; + + this.callSuper('initialize', options); + this._initRxRy(); + + this.x = options.x || 0; + this.y = options.y || 0; + }, + + /** + * Initializes rx/ry attributes + * @private + */ + _initRxRy: function() { + if (this.rx && !this.ry) { + this.ry = this.rx; + } + else if (this.ry && !this.rx) { + this.rx = this.ry; + } + }, + + /** + * @private + * @param ctx {CanvasRenderingContext2D} context to render on + */ + _render: function(ctx) { + + // optimize 1x1 case (used in spray brush) + if (this.width === 1 && this.height === 1) { + ctx.fillRect(0, 0, 1, 1); + return; + } + + var rx = this.rx || 0, + ry = this.ry || 0, + w = this.width, + h = this.height, + x = -w / 2, + y = -h / 2, + isInPathGroup = this.group && this.group.type === 'path-group', + isRounded = rx !== 0 || ry !== 0; + + ctx.beginPath(); + ctx.globalAlpha = isInPathGroup ? (ctx.globalAlpha * this.opacity) : this.opacity; + + if (this.transformMatrix && isInPathGroup) { + ctx.translate( + this.width / 2 + this.x, + this.height / 2 + this.y); + } + if (!this.transformMatrix && isInPathGroup) { + ctx.translate( + -this.group.width / 2 + this.width / 2 + this.x, + -this.group.height / 2 + this.height / 2 + this.y); + } + + ctx.moveTo(x + rx, y); + + ctx.lineTo(x + w - rx, y); + isRounded && ctx.quadraticCurveTo(x + w, y, x + w, y + ry, x + w, y + ry); + + ctx.lineTo(x + w, y + h - ry); + isRounded && ctx.quadraticCurveTo(x + w, y + h, x + w - rx, y + h, x + w - rx, y + h); + + ctx.lineTo(x + rx, y + h); + isRounded && ctx.quadraticCurveTo(x, y + h, x, y + h - ry, x, y + h - ry); + + ctx.lineTo(x, y + ry); + isRounded && ctx.quadraticCurveTo(x, y, x + rx, y, x + rx, y); + + ctx.closePath(); + + this._renderFill(ctx); + this._renderStroke(ctx); + }, + + /** + * @private + * @param ctx {CanvasRenderingContext2D} context to render on + */ + _renderDashedStroke: function(ctx) { + var x = -this.width/2, + y = -this.height/2, + w = this.width, + h = this.height; + + ctx.beginPath(); + fabric.util.drawDashedLine(ctx, x, y, x+w, y, this.strokeDashArray); + fabric.util.drawDashedLine(ctx, x+w, y, x+w, y+h, this.strokeDashArray); + fabric.util.drawDashedLine(ctx, x+w, y+h, x, y+h, this.strokeDashArray); + fabric.util.drawDashedLine(ctx, x, y+h, x, y, this.strokeDashArray); + ctx.closePath(); + }, + + /** + * Since coordinate system differs from that of SVG + * @private + */ + _normalizeLeftTopProperties: function(parsedAttributes) { + if ('left' in parsedAttributes) { + this.set('left', parsedAttributes.left + this.getWidth() / 2); + } + this.set('x', parsedAttributes.left || 0); + if ('top' in parsedAttributes) { + this.set('top', parsedAttributes.top + this.getHeight() / 2); + } + this.set('y', parsedAttributes.top || 0); + return this; + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + var object = extend(this.callSuper('toObject', propertiesToInclude), { + rx: this.get('rx') || 0, + ry: this.get('ry') || 0, + x: this.get('x'), + y: this.get('y') + }); + if (!this.includeDefaultValues) { + this._removeDefaultValues(object); + } + 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(); + + markup.push( + '' + ); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + /* _TO_SVG_END_ */ + + /** + * Returns complexity of an instance + * @return {Number} complexity + */ + complexity: function() { + return 1; + } + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Rect.fromElement`) + * @static + * @memberOf fabric.Rect + * @see: http://www.w3.org/TR/SVG/shapes.html#RectElement + */ + fabric.Rect.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('x y rx ry width height'.split(' ')); + + /** + * @private + */ + function _setDefaultLeftTopValues(attributes) { + attributes.left = attributes.left || 0; + attributes.top = attributes.top || 0; + return attributes; + } + + /** + * Returns {@link fabric.Rect} instance from an SVG element + * @static + * @memberOf fabric.Rect + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @return {fabric.Rect} Instance of fabric.Rect + */ + fabric.Rect.fromElement = function(element, options) { + if (!element) { + return null; + } + + var parsedAttributes = fabric.parseAttributes(element, fabric.Rect.ATTRIBUTE_NAMES); + parsedAttributes = _setDefaultLeftTopValues(parsedAttributes); + + var rect = new fabric.Rect(extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes)); + rect._normalizeLeftTopProperties(parsedAttributes); + + return rect; + }; + /* _FROM_SVG_END_ */ + + /** + * Returns {@link fabric.Rect} instance from an object representation + * @static + * @memberOf fabric.Rect + * @param object {Object} object to create an instance from + * @return {Object} instance of fabric.Rect + */ + fabric.Rect.fromObject = function(object) { + return new fabric.Rect(object); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + toFixed = fabric.util.toFixed; + + if (fabric.Polyline) { + fabric.warn('fabric.Polyline is already defined'); + return; + } + + /** + * 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', + + /** + * Constructor + * @param {Array} points Array of points (where each point is an object with x and y) + * @param {Object} [options] Options object + * @param {Boolean} [skipOffset] Whether points offsetting should be skipped + * @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, skipOffset) { + options = options || { }; + this.set('points', points); + this.callSuper('initialize', options); + this._calcDimensions(skipOffset); + }, + + /** + * @private + * @param {Boolean} [skipOffset] Whether points offsetting should be skipped + */ + _calcDimensions: function(skipOffset) { + return fabric.Polygon.prototype._calcDimensions.call(this, skipOffset); + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toObject: function(propertiesToInclude) { + return fabric.Polygon.prototype.toObject.call(this, propertiesToInclude); + }, + + /* _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 = [], + markup = this._createBaseSVGMarkup(); + + for (var i = 0, len = this.points.length; i < len; i++) { + points.push(toFixed(this.points[i].x, 2), ',', toFixed(this.points[i].y, 2), ' '); + } + + markup.push( + '' + ); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + /* _TO_SVG_END_ */ + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + var point; + ctx.beginPath(); + ctx.moveTo(this.points[0].x, this.points[0].y); + for (var i = 0, len = this.points.length; i < len; i++) { + point = this.points[i]; + ctx.lineTo(point.x, point.y); + } + + this._renderFill(ctx); + this._renderStroke(ctx); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderDashedStroke: function(ctx) { + var p1, p2; + + ctx.beginPath(); + for (var i = 0, len = this.points.length; i < len; i++) { + p1 = this.points[i]; + p2 = this.points[i+1] || p1; + fabric.util.drawDashedLine(ctx, p1.x, p1.y, p2.x, p2.y, this.strokeDashArray); + } + }, + + /** + * Returns complexity of an instance + * @return {Number} complexity of this instance + */ + complexity: function() { + return this.get('points').length; + } + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Polyline.fromElement}) + * @static + * @memberOf fabric.Polyline + * @see: http://www.w3.org/TR/SVG/shapes.html#PolylineElement + */ + fabric.Polyline.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(); + + /** + * Returns fabric.Polyline instance from an SVG element + * @static + * @memberOf fabric.Polyline + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @return {fabric.Polyline} Instance of fabric.Polyline + */ + fabric.Polyline.fromElement = function(element, options) { + if (!element) { + return null; + } + options || (options = { }); + + var points = fabric.parsePointsAttribute(element.getAttribute('points')), + parsedAttributes = fabric.parseAttributes(element, fabric.Polyline.ATTRIBUTE_NAMES); + + fabric.util.normalizePoints(points, options); + + return new fabric.Polyline(points, fabric.util.object.extend(parsedAttributes, options), true); + }; + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Polyline instance from an object representation + * @static + * @memberOf fabric.Polyline + * @param object {Object} object Object to create an instance from + * @return {fabric.Polyline} Instance of fabric.Polyline + */ + fabric.Polyline.fromObject = function(object) { + var points = object.points; + return new fabric.Polyline(points, object, true); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + "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; + + if (fabric.Polygon) { + fabric.warn('fabric.Polygon is already defined'); + return; + } + + /** + * Polygon class + * @class fabric.Polygon + * @extends fabric.Object + * @see {@link fabric.Polygon#initialize} for constructor definition + */ + fabric.Polygon = fabric.util.createClass(fabric.Object, /** @lends fabric.Polygon.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'polygon', + + /** + * Constructor + * @param {Array} points Array of points + * @param {Object} [options] Options object + * @param {Boolean} [skipOffset] Whether points offsetting should be skipped + * @return {fabric.Polygon} thisArg + */ + initialize: function(points, options, skipOffset) { + options = options || { }; + this.points = points; + this.callSuper('initialize', options); + this._calcDimensions(skipOffset); + }, + + /** + * @private + * @param {Boolean} [skipOffset] Whether points offsetting should be skipped + */ + _calcDimensions: function(skipOffset) { + + var points = this.points, + minX = min(points, 'x'), + minY = min(points, 'y'), + maxX = max(points, 'x'), + maxY = max(points, 'y'); + + this.width = (maxX - minX) || 1; + this.height = (maxY - minY) || 1; + + this.minX = minX; + this.minY = minY; + + if (skipOffset) return; + + var halfWidth = this.width / 2 + this.minX, + halfHeight = this.height / 2 + this.minY; + + // change points to offset polygon into a bounding box + this.points.forEach(function(p) { + p.x -= halfWidth; + p.y -= halfHeight; + }, this); + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toObject: function(propertiesToInclude) { + return extend(this.callSuper('toObject', propertiesToInclude), { + points: this.points.concat() + }); + }, + + /* _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 = [], + markup = this._createBaseSVGMarkup(); + + for (var i = 0, len = this.points.length; i < len; i++) { + points.push(toFixed(this.points[i].x, 2), ',', toFixed(this.points[i].y, 2), ' '); + } + + markup.push( + '' + ); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + /* _TO_SVG_END_ */ + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + var point; + ctx.beginPath(); + ctx.moveTo(this.points[0].x, this.points[0].y); + for (var i = 0, len = this.points.length; i < len; i++) { + point = this.points[i]; + ctx.lineTo(point.x, point.y); + } + this._renderFill(ctx); + if (this.stroke || this.strokeDashArray) { + ctx.closePath(); + this._renderStroke(ctx); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderDashedStroke: function(ctx) { + var p1, p2; + + ctx.beginPath(); + for (var i = 0, len = this.points.length; i < len; i++) { + p1 = this.points[i]; + p2 = this.points[i+1] || this.points[0]; + fabric.util.drawDashedLine(ctx, p1.x, p1.y, p2.x, p2.y, this.strokeDashArray); + } + ctx.closePath(); + }, + + /** + * Returns complexity of an instance + * @return {Number} complexity of this instance + */ + complexity: function() { + return this.points.length; + } + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Polygon.fromElement`) + * @static + * @memberOf fabric.Polygon + * @see: http://www.w3.org/TR/SVG/shapes.html#PolygonElement + */ + fabric.Polygon.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(); + + /** + * Returns {@link fabric.Polygon} instance from an SVG element + * @static + * @memberOf fabric.Polygon + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @return {fabric.Polygon} Instance of fabric.Polygon + */ + fabric.Polygon.fromElement = function(element, options) { + if (!element) { + return null; + } + options || (options = { }); + + var points = fabric.parsePointsAttribute(element.getAttribute('points')), + parsedAttributes = fabric.parseAttributes(element, fabric.Polygon.ATTRIBUTE_NAMES); + + fabric.util.normalizePoints(points, options); + + return new fabric.Polygon(points, extend(parsedAttributes, options), true); + }; + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Polygon instance from an object representation + * @static + * @memberOf fabric.Polygon + * @param object {Object} object Object to create an instance from + * @return {fabric.Polygon} Instance of fabric.Polygon + */ + fabric.Polygon.fromObject = function(object) { + return new fabric.Polygon(object.points, object, true); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + var commandLengths = { + m: 2, + l: 2, + h: 1, + v: 1, + c: 6, + s: 4, + q: 4, + t: 2, + a: 7 + }; + + "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; + + if (fabric.Path) { + fabric.warn('fabric.Path is already defined'); + return; + } + + /** + * @private + */ + function getX(item) { + if (item[0] === 'H') { + return item[1]; + } + return item[item.length - 2]; + } + + /** + * @private + */ + function getY(item) { + if (item[0] === 'V') { + return item[1]; + } + return item[item.length - 1]; + } + + /** + * Path class + * @class fabric.Path + * @extends fabric.Object + * @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', + + /** + * Constructor + * @param {Array|String} path Path data (sequence of coordinates and corresponding "command" tokens) + * @param {Object} [options] Options object + * @return {fabric.Path} thisArg + */ + initialize: function(path, options) { + options = options || { }; + + this.setOptions(options); + + if (!path) { + throw new Error('`path` argument is required'); + } + + var fromArray = _toString.call(path) === '[object Array]'; + + this.path = fromArray + ? path + // one of commands (m,M,l,L,q,Q,c,C,etc.) followed by non-command characters (i.e. command values) + : path.match && path.match(/[mzlhvcsqta][^mzlhvcsqta]*/gi); + + if (!this.path) return; + + if (!fromArray) { + this.path = this._parsePath(); + } + this._initializePath(options); + + if (options.sourcePath) { + this.setSourcePath(options.sourcePath); + } + }, + + /** + * @private + * @param {Object} [options] Options object + */ + _initializePath: function (options) { + var isWidthSet = 'width' in options && options.width != null, + isHeightSet = 'height' in options && options.width != null, + isLeftSet = 'left' in options, + isTopSet = 'top' in options, + origLeft = isLeftSet ? this.left : 0, + origTop = isTopSet ? this.top : 0; + + if (!isWidthSet || !isHeightSet) { + extend(this, this._parseDimensions()); + if (isWidthSet) { + this.width = options.width; + } + if (isHeightSet) { + this.height = options.height; + } + } + else { //Set center location relative to given height/width if not specified + if (!isTopSet) { + this.top = this.height / 2; + } + if (!isLeftSet) { + this.left = this.width / 2; + } + } + this.pathOffset = this.pathOffset || + // Save top-left coords as offset + this._calculatePathOffset(origLeft, origTop); + }, + + /** + * @private + * @param {Boolean} positionSet When false, path offset is returned otherwise 0 + */ + _calculatePathOffset: function (origLeft, origTop) { + return { + x: this.left - origLeft - (this.width / 2), + y: this.top - origTop - (this.height / 2) + }; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx context to render path on + */ + _render: function(ctx) { + var current, // current instruction + previous = null, + x = 0, // current x + y = 0, // current y + controlX = 0, // current control point x + controlY = 0, // current control point y + tempX, + tempY, + tempControlX, + tempControlY, + l = -((this.width / 2) + this.pathOffset.x), + t = -((this.height / 2) + this.pathOffset.y), + methodName; + + for (var i = 0, len = this.path.length; i < len; ++i) { + + current = this.path[i]; + + switch (current[0]) { // first letter + + case 'l': // lineto, relative + x += current[1]; + y += current[2]; + ctx.lineTo(x + l, y + t); + break; + + case 'L': // lineto, absolute + x = current[1]; + y = current[2]; + ctx.lineTo(x + l, y + t); + break; + + case 'h': // horizontal lineto, relative + x += current[1]; + ctx.lineTo(x + l, y + t); + break; + + case 'H': // horizontal lineto, absolute + x = current[1]; + ctx.lineTo(x + l, y + t); + break; + + case 'v': // vertical lineto, relative + y += current[1]; + ctx.lineTo(x + l, y + t); + break; + + case 'V': // verical lineto, absolute + y = current[1]; + ctx.lineTo(x + l, y + t); + break; + + case 'm': // moveTo, relative + x += current[1]; + y += current[2]; + // draw a line if previous command was moveTo as well (otherwise, it will have no effect) + methodName = (previous && (previous[0] === 'm' || previous[0] === 'M')) + ? 'lineTo' + : 'moveTo'; + ctx[methodName](x + l, y + t); + break; + + case 'M': // moveTo, absolute + x = current[1]; + y = current[2]; + // draw a line if previous command was moveTo as well (otherwise, it will have no effect) + methodName = (previous && (previous[0] === 'm' || previous[0] === 'M')) + ? 'lineTo' + : 'moveTo'; + ctx[methodName](x + l, y + t); + break; + + case 'c': // bezierCurveTo, relative + tempX = x + current[5]; + tempY = y + current[6]; + controlX = x + current[3]; + controlY = y + current[4]; + ctx.bezierCurveTo( + x + current[1] + l, // x1 + y + current[2] + t, // y1 + controlX + l, // x2 + controlY + t, // y2 + tempX + l, + tempY + t + ); + x = tempX; + y = tempY; + break; + + case 'C': // bezierCurveTo, absolute + x = current[5]; + y = current[6]; + controlX = current[3]; + controlY = current[4]; + ctx.bezierCurveTo( + current[1] + l, + current[2] + t, + controlX + l, + controlY + t, + x + l, + y + t + ); + break; + + case 's': // shorthand cubic bezierCurveTo, relative + + // transform to absolute x,y + tempX = x + current[3]; + tempY = y + current[4]; + + // calculate reflection of previous control points + controlX = controlX ? (2 * x - controlX) : x; + controlY = controlY ? (2 * y - controlY) : y; + + ctx.bezierCurveTo( + controlX + l, + controlY + t, + x + current[1] + l, + y + current[2] + t, + tempX + l, + tempY + t + ); + // set control point to 2nd one of this command + // "... the first control point is assumed to be + // the reflection of the second control point on + // the previous command relative to the current point." + controlX = x + current[1]; + controlY = y + current[2]; + + x = tempX; + y = tempY; + break; + + case 'S': // shorthand cubic bezierCurveTo, absolute + tempX = current[3]; + tempY = current[4]; + // calculate reflection of previous control points + controlX = 2*x - controlX; + controlY = 2*y - controlY; + ctx.bezierCurveTo( + controlX + l, + controlY + t, + current[1] + l, + current[2] + t, + tempX + l, + tempY + t + ); + x = tempX; + y = tempY; + + // set control point to 2nd one of this command + // "... the first control point is assumed to be + // the reflection of the second control point on + // the previous command relative to the current point." + controlX = current[1]; + controlY = current[2]; + + break; + + case 'q': // quadraticCurveTo, relative + // transform to absolute x,y + tempX = x + current[3]; + tempY = y + current[4]; + + controlX = x + current[1]; + controlY = y + current[2]; + + ctx.quadraticCurveTo( + controlX + l, + controlY + t, + tempX + l, + tempY + t + ); + x = tempX; + y = tempY; + break; + + case 'Q': // quadraticCurveTo, absolute + tempX = current[3]; + tempY = current[4]; + + ctx.quadraticCurveTo( + current[1] + l, + current[2] + t, + tempX + l, + tempY + t + ); + x = tempX; + y = tempY; + controlX = current[1]; + controlY = current[2]; + break; + + case 't': // shorthand quadraticCurveTo, relative + + // transform to absolute x,y + tempX = x + current[1]; + tempY = y + current[2]; + + + if (previous[0].match(/[QqTt]/) === null) { + // If there is no previous command or if the previous command was not a Q, q, T or t, + // assume the control point is coincident with the current point + controlX = x; + controlY = y; + } + else if (previous[0] === 't') { + // calculate reflection of previous control points for t + controlX = 2 * x - tempControlX; + controlY = 2 * y - tempControlY; + } + else if (previous[0] === 'q') { + // calculate reflection of previous control points for q + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + + tempControlX = controlX; + tempControlY = controlY; + + ctx.quadraticCurveTo( + controlX + l, + controlY + t, + tempX + l, + tempY + t + ); + x = tempX; + y = tempY; + controlX = x + current[1]; + controlY = y + current[2]; + break; + + case 'T': + tempX = current[1]; + tempY = current[2]; + + // calculate reflection of previous control points + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + ctx.quadraticCurveTo( + controlX + l, + controlY + t, + tempX + l, + tempY + t + ); + x = tempX; + y = tempY; + break; + + case 'a': + // TODO: optimize this + drawArc(ctx, x + l, y + t, [ + current[1], + current[2], + current[3], + current[4], + current[5], + current[6] + x + l, + current[7] + y + t + ]); + x += current[6]; + y += current[7]; + break; + + case 'A': + // TODO: optimize this + drawArc(ctx, x + l, y + t, [ + current[1], + current[2], + current[3], + current[4], + current[5], + current[6] + l, + current[7] + t + ]); + x = current[6]; + y = current[7]; + break; + + case 'z': + case 'Z': + ctx.closePath(); + break; + } + previous = current; + } + }, + + /** + * Renders path on a specified context + * @param {CanvasRenderingContext2D} ctx context to render path on + * @param {Boolean} [noTransform] When true, context is not transformed + */ + render: function(ctx, noTransform) { + // do not render if object is not visible + if (!this.visible) return; + + ctx.save(); + var m = this.transformMatrix; + if (m) { + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + if (!noTransform) { + this.transform(ctx); + } + this._setStrokeStyles(ctx); + this._setFillStyles(ctx); + this._setShadow(ctx); + this.clipTo && fabric.util.clipContext(this, ctx); + ctx.beginPath(); + + this._render(ctx); + this._renderFill(ctx); + this._renderStroke(ctx); + this.clipTo && ctx.restore(); + this._removeShadow(ctx); + + if (!noTransform && this.active) { + this.drawBorders(ctx); + this.drawControls(ctx); + } + ctx.restore(); + }, + + /** + * Returns string representation of an instance + * @return {String} string representation of an instance + */ + toString: function() { + return '#'; + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + var o = extend(this.callSuper('toObject', propertiesToInclude), { + path: this.path.map(function(item) { return item.slice() }), + pathOffset: this.pathOffset + }); + if (this.sourcePath) { + o.sourcePath = this.sourcePath; + } + if (this.transformMatrix) { + o.transformMatrix = this.transformMatrix; + } + return o; + }, + + /** + * Returns dataless object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toDatalessObject: function(propertiesToInclude) { + var o = this.toObject(propertiesToInclude); + if (this.sourcePath) { + o.path = this.sourcePath; + } + delete o.sourcePath; + return o; + }, + + /* _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(); + + for (var i = 0, len = this.path.length; i < len; i++) { + chunks.push(this.path[i].join(' ')); + } + var path = chunks.join(' '); + + markup.push( + '', + '', + '' + ); + + 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].toLowerCase(), + commandLength = commandLengths[command]; + + if (coordsParsed.length - 1 > commandLength) { + for (var k = 1, klen = coordsParsed.length; k < klen; k += commandLength) { + result.push([ coordsParsed[0] ].concat(coordsParsed.slice(k, k + commandLength))); + } + } + else { + result.push(coordsParsed); + } + } + + return result; + }, + + /** + * @private + */ + _parseDimensions: function() { + var aX = [], + aY = [], + previous = { }; + + this.path.forEach(function(item, i) { + this._getCoordsFromCommand(item, i, aX, aY, previous); + }, this); + + var minX = min(aX), + minY = min(aY), + maxX = max(aX), + maxY = max(aY), + deltaX = maxX - minX, + deltaY = maxY - minY; + + var o = { + left: this.left + (minX + deltaX / 2), + top: this.top + (minY + deltaY / 2), + width: deltaX, + height: deltaY + }; + + return o; + }, + + _getCoordsFromCommand: function(item, i, aX, aY, previous) { + var isLowerCase = false; + + if (item[0] !== 'H') { + previous.x = (i === 0) ? getX(item) : getX(this.path[i - 1]); + } + if (item[0] !== 'V') { + previous.y = (i === 0) ? getY(item) : getY(this.path[i - 1]); + } + + // lowercased letter denotes relative position; + // transform to absolute + if (item[0] === item[0].toLowerCase()) { + isLowerCase = true; + } + + var xy = this._getXY(item, isLowerCase, previous); + + var val = parseInt(xy.x, 10); + if (!isNaN(val)) aX.push(val); + + val = parseInt(xy.y, 10); + if (!isNaN(val)) aY.push(val); + }, + + _getXY: function(item, isLowerCase, previous) { + + // last 2 items in an array of coordinates are the actualy x/y (except H/V), collect them + // TODO (kangax): support relative h/v commands + + var x = isLowerCase + ? previous.x + getX(item) + : item[0] === 'V' + ? previous.x + : getX(item); + + var y = isLowerCase + ? previous.y + getY(item) + : item[0] === 'H' + ? previous.y + : getY(item); + + return { x: x, y: y }; + } + }); + + /** + * Creates an instance of fabric.Path from an object + * @static + * @memberOf fabric.Path + * @param {Object} object + * @param {Function} callback Callback to invoke when an fabric.Path instance is created + */ + fabric.Path.fromObject = function(object, callback) { + if (typeof object.path === 'string') { + fabric.loadSVGFromURL(object.path, function (elements) { + var path = elements[0]; + + var pathUrl = object.path; + delete object.path; + + fabric.util.object.extend(path, object); + path.setSourcePath(pathUrl); + + callback(path); + }); + } + else { + callback(new fabric.Path(object.path, object)); + } + }; + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Path.fromElement`) + * @static + * @memberOf fabric.Path + * @see http://www.w3.org/TR/SVG/paths.html#PathElement + */ + fabric.Path.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(['d']); + + /** + * Creates an instance of fabric.Path from an SVG element + * @static + * @memberOf fabric.Path + * @param {SVGElement} element to parse + * @param {Function} callback Callback to invoke when an fabric.Path instance is created + * @param {Object} [options] Options object + */ + fabric.Path.fromElement = function(element, callback, options) { + var parsedAttributes = fabric.parseAttributes(element, fabric.Path.ATTRIBUTE_NAMES); + callback && callback(new fabric.Path(parsedAttributes.d, extend(parsedAttributes, options))); + }; + /* _FROM_SVG_END_ */ + + /** + * Indicates that instances of this type are async + * @static + * @memberOf fabric.Path + * @type Boolean + * @default + */ + fabric.Path.async = true; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + invoke = fabric.util.array.invoke, + parentToObject = fabric.Object.prototype.toObject; + + if (fabric.PathGroup) { + fabric.warn('fabric.PathGroup is already defined'); + return; + } + + /** + * Path group class + * @class fabric.PathGroup + * @extends fabric.Path + * @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.Path, /** @lends fabric.PathGroup.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'path-group', + + /** + * Fill value + * @type String + * @default + */ + fill: '', + + /** + * Constructor + * @param {Array} paths + * @param {Object} [options] Options object + * @return {fabric.PathGroup} thisArg + */ + initialize: function(paths, options) { + + options = options || { }; + this.paths = paths || [ ]; + + for (var i = this.paths.length; i--; ) { + this.paths[i].group = this; + } + + this.setOptions(options); + this.setCoords(); + + if (options.sourcePath) { + this.setSourcePath(options.sourcePath); + } + }, + + /** + * Renders this group on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render this instance on + */ + render: function(ctx) { + // do not render if object is not visible + if (!this.visible) return; + + ctx.save(); + + var m = this.transformMatrix; + if (m) { + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + + this.transform(ctx); + + this._setShadow(ctx); + this.clipTo && fabric.util.clipContext(this, ctx); + for (var i = 0, l = this.paths.length; i < l; ++i) { + this.paths[i].render(ctx, true); + } + this.clipTo && ctx.restore(); + this._removeShadow(ctx); + + if (this.active) { + this.drawBorders(ctx); + this.drawControls(ctx); + } + ctx.restore(); + }, + + /** + * Sets certain property to a certain value + * @param {String} prop + * @param {Any} value + * @return {fabric.PathGroup} thisArg + */ + _set: function(prop, value) { + + if (prop === 'fill' && 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 o = extend(parentToObject.call(this, propertiesToInclude), { + paths: invoke(this.getObjects(), 'toObject', propertiesToInclude) + }); + if (this.sourcePath) { + o.sourcePath = this.sourcePath; + } + 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(); + var markup = [ + '' + ]; + + for (var i = 0, len = objects.length; i < len; i++) { + markup.push(objects[i].toSVG(reviver)); + } + markup.push(''); + + 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'); + return this.getObjects().every(function(path) { + return path.get('fill') === firstPathFill; + }); + }, + + /** + * Returns number representation of object's complexity + * @return {Number} complexity + */ + complexity: function() { + return this.paths.reduce(function(total, path) { + return total + ((path && path.complexity) ? path.complexity() : 0); + }, 0); + }, + + /** + * 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) { + if (typeof object.paths === 'string') { + fabric.loadSVGFromURL(object.paths, function (elements) { + + var pathUrl = object.paths; + delete object.paths; + + var pathGroup = fabric.util.groupSVGElements(elements, object, pathUrl); + + callback(pathGroup); + }); + } + else { + fabric.util.enlivenObjects(object.paths, function(enlivenedObjects) { + delete object.paths; + callback(new fabric.PathGroup(enlivenedObjects, object)); + }); + } + }; + + /** + * Indicates that instances of this type are async + * @static + * @memberOf fabric.PathGroup + * @type Boolean + * @default + */ + fabric.PathGroup.async = true; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global){ + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + min = fabric.util.array.min, + max = fabric.util.array.max, + invoke = fabric.util.array.invoke; + + if (fabric.Group) { + return; + } + + // lock-related properties, for use in fabric.Group#get + // to enable locking behavior on group + // when one of its objects has lock-related properties set + var _lockProperties = { + lockMovementX: true, + lockMovementY: true, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + lockUniScaling: true + }; + + /** + * Group class + * @class fabric.Group + * @extends fabric.Object + * @mixes fabric.Collection + * @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', + + /** + * Constructor + * @param {Object} objects Group objects + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(objects, options) { + options = options || { }; + + this._objects = objects || []; + for (var i = this._objects.length; i--; ) { + this._objects[i].group = this; + } + + this.originalState = { }; + this.callSuper('initialize'); + + this._calcBounds(); + this._updateObjectsCoords(); + + if (options) { + extend(this, options); + } + this._setOpacityIfSame(); + + this.setCoords(true); + this.saveCoords(); + }, + + /** + * @private + */ + _updateObjectsCoords: function() { + this.forEachObject(this._updateObjectCoords, this); + }, + + /** + * @private + */ + _updateObjectCoords: function(object) { + var objectLeft = object.getLeft(), + objectTop = object.getTop(); + + object.set({ + originalLeft: objectLeft, + originalTop: objectTop, + left: objectLeft - this.left, + top: objectTop - this.top + }); + + object.setCoords(); + + // do not display corners of objects enclosed in a group + object.__origHasControls = object.hasControls; + object.hasControls = false; + }, + + /** + * 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(); + this._objects.push(object); + object.group = this; + // since _restoreObjectsState set objects inactive + this.forEachObject(this._setObjectActive, this); + this._calcBounds(); + this._updateObjectsCoords(); + 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._moveFlippedObject(object); + this._restoreObjectsState(); + + // since _restoreObjectsState set objects inactive + this.forEachObject(this._setObjectActive, this); + + this.remove(object); + this._calcBounds(); + this._updateObjectsCoords(); + + return this; + }, + + /** + * @private + */ + _onObjectAdded: function(object) { + object.group = this; + }, + + /** + * @private + */ + _onObjectRemoved: function(object) { + delete object.group; + object.set('active', false); + }, + + /** + * Properties that are delegated to group objects when reading/writing + * @param {Object} delegatedProperties + */ + delegatedProperties: { + fill: true, + opacity: true, + fontFamily: true, + fontWeight: true, + fontSize: true, + fontStyle: true, + lineHeight: true, + textDecoration: true, + textAlign: true, + backgroundColor: true + }, + + /** + * @private + */ + _set: function(key, value) { + if (key in this.delegatedProperties) { + var i = this._objects.length; + this[key] = value; + while (i--) { + this._objects[i].set(key, value); + } + } + else { + this[key] = value; + } + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] 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), { + objects: invoke(this._objects, 'toObject', propertiesToInclude) + }); + }, + + /** + * Renders instance on a given context + * @param {CanvasRenderingContext2D} ctx context to render instance on + * @param {Boolean} [noTransform] When true, context is not transformed + */ + render: function(ctx, noTransform) { + // do not render if object is not visible + if (!this.visible) return; + + ctx.save(); + this.transform(ctx); + + this.clipTo && fabric.util.clipContext(this, ctx); + + // the array is now sorted in order of highest first, so start from end + for (var i = 0, len = this._objects.length; i < len; i++) { + this._renderObject(this._objects[i], ctx); + } + + this.clipTo && ctx.restore(); + + if (!noTransform && this.active) { + this.drawBorders(ctx); + this.drawControls(ctx); + } + ctx.restore(); + }, + + /** + * @private + */ + _renderObject: function(object, ctx) { + + var originalScaleFactor = object.borderScaleFactor, + originalHasRotatingPoint = object.hasRotatingPoint, + groupScaleFactor = Math.max(this.scaleX, this.scaleY); + + // do not render if object is not visible + if (!object.visible) return; + + object.borderScaleFactor = groupScaleFactor; + object.hasRotatingPoint = false; + + object.render(ctx); + + object.borderScaleFactor = originalScaleFactor; + 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; + }, + + /** + * Moves a flipped object to the position where it's displayed + * @private + * @param {fabric.Object} object + * @return {fabric.Group} thisArg + */ + _moveFlippedObject: function(object) { + var oldOriginX = object.get('originX'), + oldOriginY = object.get('originY'), + center = object.getCenterPoint(); + + object.set({ + originX: 'center', + originY: 'center', + left: center.x, + top: center.y + }); + + this._toggleFlipping(object); + + var newOrigin = object.getPointByOrigin(oldOriginX, oldOriginY); + + object.set({ + originX: oldOriginX, + originY: oldOriginY, + left: newOrigin.x, + top: newOrigin.y + }); + + return this; + }, + + /** + * @private + */ + _toggleFlipping: function(object) { + if (this.flipX) { + object.toggle('flipX'); + object.set('left', -object.get('left')); + object.setAngle(-object.getAngle()); + } + if (this.flipY) { + object.toggle('flipY'); + object.set('top', -object.get('top')); + object.setAngle(-object.getAngle()); + } + }, + + /** + * Restores original state of a specified object in group + * @private + * @param {fabric.Object} object + * @return {fabric.Group} thisArg + */ + _restoreObjectState: function(object) { + this._setObjectPosition(object); + + object.setCoords(); + object.hasControls = object.__origHasControls; + delete object.__origHasControls; + object.set('active', false); + object.setCoords(); + delete object.group; + + return this; + }, + + /** + * @private + */ + _setObjectPosition: function(object) { + var groupLeft = this.getLeft(), + groupTop = this.getTop(), + rotated = this._getRotatedLeftTop(object); + + object.set({ + angle: object.getAngle() + this.getAngle(), + left: groupLeft + rotated.left, + top: groupTop + rotated.top, + scaleX: object.get('scaleX') * this.get('scaleX'), + scaleY: object.get('scaleY') * this.get('scaleY') + }); + }, + + /** + * @private + */ + _getRotatedLeftTop: function(object) { + var groupAngle = this.getAngle() * (Math.PI / 180); + return { + left: (-Math.sin(groupAngle) * object.getTop() * this.get('scaleY') + + Math.cos(groupAngle) * object.getLeft() * this.get('scaleX')), + + top: (Math.cos(groupAngle) * object.getTop() * this.get('scaleY') + + Math.sin(groupAngle) * object.getLeft() * this.get('scaleX')) + }; + }, + + /** + * Destroys a group (restoring state of its objects) + * @return {fabric.Group} thisArg + * @chainable + */ + destroy: function() { + this._objects.forEach(this._moveFlippedObject, this); + return this._restoreObjectsState(); + }, + + /** + * Saves coordinates of this instance (to be used together with `hasMoved`) + * @saveCoords + * @return {fabric.Group} thisArg + * @chainable + */ + saveCoords: function() { + this._originalLeft = this.get('left'); + this._originalTop = this.get('top'); + return this; + }, + + /** + * Checks whether this group was moved (since `saveCoords` was called last) + * @return {Boolean} true if an object was moved (since fabric.Group#saveCoords was called) + */ + hasMoved: function() { + return this._originalLeft !== this.get('left') || + this._originalTop !== this.get('top'); + }, + + /** + * Sets coordinates of all group objects + * @return {fabric.Group} thisArg + * @chainable + */ + setObjectsCoords: function() { + this.forEachObject(function(object) { + object.setCoords(); + }); + return this; + }, + + /** + * @private + */ + _setOpacityIfSame: function() { + var objects = this.getObjects(), + firstValue = objects[0] ? objects[0].get('opacity') : 1; + + var isSameOpacity = objects.every(function(o) { + return o.get('opacity') === firstValue; + }); + + if (isSameOpacity) { + this.opacity = firstValue; + } + }, + + /** + * @private + */ + _calcBounds: function() { + var aX = [], + aY = [], + o; + + for (var i = 0, len = this._objects.length; i < len; ++i) { + o = this._objects[i]; + o.setCoords(); + for (var prop in o.oCoords) { + aX.push(o.oCoords[prop].x); + aY.push(o.oCoords[prop].y); + } + } + + this.set(this._getBounds(aX, aY)); + }, + + /** + * @private + */ + _getBounds: function(aX, aY) { + var minX = min(aX), + maxX = max(aX), + minY = min(aY), + maxY = max(aY), + width = (maxX - minX) || 0, + height = (maxY - minY) || 0; + + return { + width: width, + height: height, + left: (minX + width / 2) || 0, + top: (minY + height / 2) || 0 + }; + }, + + /* _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 = [ + '' + ]; + + for (var i = 0, len = this._objects.length; i < len; i++) { + markup.push(this._objects[i].toSVG(reviver)); + } + + markup.push(''); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + /* _TO_SVG_END_ */ + + /** + * Returns requested property + * @param {String} prop Property to get + * @return {Any} + */ + get: function(prop) { + if (prop in _lockProperties) { + if (this[prop]) { + return this[prop]; + } + else { + for (var i = 0, len = this._objects.length; i < len; i++) { + if (this._objects[i][prop]) { + return true; + } + } + return false; + } + } + else { + if (prop in this.delegatedProperties) { + return this._objects[0] && this._objects[0].get(prop); + } + return this[prop]; + } + } + }); + + /** + * Returns {@link fabric.Group} instance from an object representation + * @static + * @memberOf fabric.Group + * @param {Object} object Object to create a group from + * @param {Object} [options] Options object + * @return {fabric.Group} An instance of fabric.Group + */ + fabric.Group.fromObject = function(object, callback) { + fabric.util.enlivenObjects(object.objects, function(enlivenedObjects) { + delete object.objects; + callback && callback(new fabric.Group(enlivenedObjects, object)); + }); + }; + + /** + * Indicates that instances of this type are async + * @static + * @memberOf fabric.Group + * @type Boolean + * @default + */ + fabric.Group.async = true; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + "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; + } + + /** + * 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", "allow-credentials") + * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes + * @type String + * @default + */ + crossOrigin: '', + + /** + * Constructor + * @param {HTMLImageElement | String} element Image element + * @param {Object} [options] Options object + * @return {fabric.Image} thisArg + */ + initialize: function(element, options) { + options || (options = { }); + + this.filters = [ ]; + + this.callSuper('initialize', options); + + this._initElement(element, options); + this._initConfig(options); + + if (options.filters) { + this.filters = options.filters; + this.applyFilters(); + } + }, + + /** + * Returns image element which this instance if based on + * @return {HTMLImageElement} Image element + */ + getElement: function() { + return this._element; + }, + + /** + * Sets image element for this instance to a specified one. + * If filters defined they are applied to new image. + * You might need to call `canvas.renderAll` and `object.setCoords` after replacing, to render new image and update controls area. + * @param {HTMLImageElement} element + * @param {Function} [callback] Callback is invoked when all filters have been applied and new image is generated + * @return {fabric.Image} thisArg + * @chainable + */ + setElement: function(element, callback) { + this._element = element; + this._originalElement = element; + this._initConfig(); + + if (this.filters.length !== 0) { + this.applyFilters(callback); + } + + return this; + }, + + /** + * 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 + }; + }, + + /** + * Renders image on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Boolean} [noTransform] When true, context is not transformed + */ + render: function(ctx, noTransform) { + // do not render if object is not visible + if (!this.visible) return; + + ctx.save(); + var m = this.transformMatrix; + var isInPathGroup = this.group && this.group.type === 'path-group'; + + // this._resetWidthHeight(); + if (isInPathGroup) { + ctx.translate(-this.group.width/2 + this.width/2, -this.group.height/2 + this.height/2); + } + if (m) { + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + if (!noTransform) { + this.transform(ctx); + } + + ctx.save(); + this._setShadow(ctx); + this.clipTo && fabric.util.clipContext(this, ctx); + this._render(ctx); + if (this.shadow && !this.shadow.affectStroke) { + this._removeShadow(ctx); + } + this._renderStroke(ctx); + this.clipTo && ctx.restore(); + ctx.restore(); + + if (this.active && !noTransform) { + this.drawBorders(ctx); + this.drawControls(ctx); + } + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _stroke: function(ctx) { + ctx.save(); + this._setStrokeStyles(ctx); + ctx.beginPath(); + ctx.strokeRect(-this.width / 2, -this.height / 2, this.width, this.height); + ctx.closePath(); + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderDashedStroke: function(ctx) { + var x = -this.width/2, + y = -this.height/2, + w = this.width, + h = this.height; + + ctx.save(); + 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) { + return extend(this.callSuper('toObject', propertiesToInclude), { + src: this._originalElement.src || this._originalElement._src, + filters: this.filters.map(function(filterObj) { + return filterObj && filterObj.toObject(); + }), + crossOrigin: this.crossOrigin + }); + }, + + /* _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 = []; + + markup.push( + '', + '' + ); + + if (this.stroke || this.strokeDashArray) { + var origFill = this.fill; + this.fill = null; + markup.push( + '' + ); + this.fill = origFill; + } + + markup.push(''); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + /* _TO_SVG_END_ */ + + /** + * Returns source of an image + * @return {String} Source of an image + */ + getSrc: function() { + return this.getElement().src || this.getElement()._src; + }, + + /** + * Returns string representation of an instance + * @return {String} String representation of an instance + */ + toString: function() { + return '#'; + }, + + /** + * Returns a clone of an instance + * @param {Function} callback Callback is invoked with a clone as a first argument + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + */ + clone: function(callback, propertiesToInclude) { + this.constructor.fromObject(this.toObject(propertiesToInclude), callback); + }, + + /** + * Applies filters assigned to this image (from "filters" array) + * @mthod applyFilters + * @param {Function} callback Callback is invoked when all filters have been applied and new image is generated + * @return {fabric.Image} thisArg + * @chainable + */ + applyFilters: function(callback) { + + if (this.filters.length === 0) { + this._element = this._originalElement; + callback && callback(); + return; + } + + var imgEl = this._originalElement, + canvasEl = fabric.util.createCanvasElement(), + replacement = fabric.util.createImage(), + _this = this; + + canvasEl.width = imgEl.width; + canvasEl.height = imgEl.height; + + canvasEl.getContext('2d').drawImage(imgEl, 0, 0, imgEl.width, imgEl.height); + + this.filters.forEach(function(filter) { + filter && filter.applyTo(canvasEl); + }); + + /** @ignore */ + + replacement.width = imgEl.width; + replacement.height = imgEl.height; + + if (fabric.isLikelyNode) { + replacement.src = canvasEl.toBuffer(undefined, fabric.Image.pngCompression); + + // onload doesn't fire in some node versions, so we invoke callback manually + _this._element = replacement; + callback && callback(); + } + else { + replacement.onload = function() { + _this._element = replacement; + callback && callback(); + replacement.onload = canvasEl = imgEl = null; + }; + replacement.src = canvasEl.toDataURL('image/png'); + } + + return this; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + ctx.drawImage( + this._element, + -this.width / 2, + -this.height / 2, + this.width, + this.height + ); + }, + + /** + * @private + */ + _resetWidthHeight: function() { + var element = this.getElement(); + + this.set('width', element.width); + this.set('height', element.height); + }, + + /** + * The Image class's initialization method. This method is automatically + * called by the constructor. + * @private + * @param {HTMLImageElement|String} element The element representing the image + */ + _initElement: function(element) { + this.setElement(fabric.util.getById(element)); + fabric.util.addClass(this.getElement(), fabric.Image.CSS_CANVAS); + }, + + /** + * @private + * @param {Object} [options] Options object + */ + _initConfig: function(options) { + options || (options = { }); + this.setOptions(options); + this._setWidthHeight(options); + this._element.crossOrigin = this.crossOrigin; + }, + + /** + * @private + * @param {Object} object Object with filters property + * @param {Function} callback Callback to invoke when all fabric.Image.filters instances are created + */ + _initFilters: function(object, callback) { + if (object.filters && object.filters.length) { + fabric.util.enlivenObjects(object.filters, function(enlivenedObjects) { + callback && callback(enlivenedObjects); + }, 'fabric.Image.filters'); + } + else { + callback && callback(); + } + }, + + /** + * @private + * @param {Object} [options] Object with width/height properties + */ + _setWidthHeight: function(options) { + this.width = 'width' in options + ? options.width + : (this.getElement().width || 0); + + this.height = 'height' in options + ? options.height + : (this.getElement().height || 0); + }, + + /** + * Returns complexity of an instance + * @return {Number} complexity of this instance + */ + complexity: function() { + return 1; + } + }); + + /** + * Default CSS class name for canvas + * @static + * @type String + * @default + */ + fabric.Image.CSS_CANVAS = "canvas-img"; + + /** + * Alias for getSrc + * @static + */ + fabric.Image.prototype.getSvgSrc = fabric.Image.prototype.getSrc; + + /** + * Creates an instance of fabric.Image from its object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when an image instance is created + */ + fabric.Image.fromObject = function(object, callback) { + fabric.util.loadImage(object.src, function(img) { + fabric.Image.prototype._initFilters.call(object, object, function(filters) { + object.filters = filters || [ ]; + var instance = new fabric.Image(img, object); + callback && callback(instance); + }); + }, 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(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 xlink:href'.split(' ')); + + /** + * Returns {@link fabric.Image} instance from an SVG element + * @static + * @param {SVGElement} element Element to parse + * @param {Function} callback Callback to execute when fabric.Image object is created + * @param {Object} [options] Options object + * @return {fabric.Image} Instance of fabric.Image + */ + fabric.Image.fromElement = function(element, callback, options) { + var parsedAttributes = fabric.parseAttributes(element, fabric.Image.ATTRIBUTE_NAMES); + + fabric.Image.fromURL(parsedAttributes['xlink:href'], callback, + extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes)); + }; + /* _FROM_SVG_END_ */ + + /** + * Indicates that instances of this type are async + * @static + * @type Boolean + * @default + */ + fabric.Image.async = true; + + /** + * Indicates compression level used when generating PNG under Node (in applyFilters). Any of 0-9 + * @static + * @type Number + * @default + */ + fabric.Image.pngCompression = 1; + +})(typeof exports !== 'undefined' ? exports : this); + + +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * @private + * @return {Number} angle value + */ + _getAngleValueForStraighten: function() { + var angle = this.getAngle() % 360; + if (angle > 0) { + return Math.round((angle-1)/90) * 90; + } + return Math.round(angle/90) * 90; + }, + + /** + * Straightens an object (rotating it from current angle to one of 0, 90, 180, 270, etc. depending on which is closer) + * @return {fabric.Object} thisArg + * @chainable + */ + straighten: function() { + this.setAngle(this._getAngleValueForStraighten()); + return this; + }, + + /** + * Same as {@link fabric.Object.prototype.straighten} but with animation + * @param {Object} callbacks Object with callback functions + * @param {Function} [callbacks.onComplete] Invoked on completion + * @param {Function} [callbacks.onChange] Invoked on every step of animation + * @return {fabric.Object} thisArg + * @chainable + */ + fxStraighten: function(callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: this.get('angle'), + endValue: this._getAngleValueForStraighten(), + duration: this.FX_DURATION, + onChange: function(value) { + _this.setAngle(value); + onChange(); + }, + onComplete: function() { + _this.setCoords(); + onComplete(); + }, + onStart: function() { + _this.set('active', false); + } + }); + + return this; + } +}); + +fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { + + /** + * Straightens object, then rerenders canvas + * @param {fabric.Object} object Object to straighten + * @return {fabric.Canvas} thisArg + * @chainable + */ + straightenObject: function (object) { + object.straighten(); + this.renderAll(); + return this; + }, + + /** + * Same as {@link fabric.Canvas.prototype.straightenObject}, but animated + * @param {fabric.Object} object Object to straighten + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxStraightenObject: function (object) { + object.fxStraighten({ + onChange: this.renderAll.bind(this) + }); + return this; + } +}); + + +/** + * @namespace fabric.Image.filters + * @memberOf fabric.Image + * @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', + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return { type: this.type }; + }, + + /** + * Returns a JSON representation of an instance + * @return {Object} JSON + */ + toJSON: function() { + // delegate, not alias + return this.toObject(); + } +}); + + +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend; + + /** + * Brightness filter class + * @class fabric.Image.filters.Brightness + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @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)); + */ + fabric.Image.filters.Brightness = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Brightness.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Brightness', + + /** + * Constructor + * @memberOf fabric.Image.filters.Brightness.prototype + * @param {Object} [options] Options object + * @param {Number} [options.brightness=100] Value to brighten the image up (0..255) + */ + initialize: function(options) { + options = options || { }; + this.brightness = options.brightness || 100; + }, + + /** + * Applies filter to canvas element + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + brightness = this.brightness; + + for (var i = 0, len = data.length; i < len; i += 4) { + data[i] += brightness; + data[i + 1] += brightness; + data[i + 2] += brightness; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + brightness: this.brightness + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @return {fabric.Image.filters.Brightness} Instance of fabric.Image.filters.Brightness + */ + fabric.Image.filters.Brightness.fromObject = function(object) { + return new fabric.Image.filters.Brightness(object); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend; + + /** + * Adapted from html5rocks article + * @class fabric.Image.filters.Convolute + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @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)); + */ + fabric.Image.filters.Convolute = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Convolute.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Convolute', + + /** + * Constructor + * @memberOf fabric.Image.filters.Convolute.prototype + * @param {Object} [options] Options object + * @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 ]; + + var canvasEl = fabric.util.createCanvasElement(); + this.tmpCtx = canvasEl.getContext('2d'); + }, + + /** + * @private + */ + _createImageData: function(w, h) { + return this.tmpCtx.createImageData(w, h); + }, + + /** + * Applies filter to canvas element + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + + var weights = this.matrix, + 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; + + // pad output by the convolution matrix + var w = sw; + var h = sh; + var output = this._createImageData(w, h); + + var dst = output.data; + + // go through the destination image pixels + var alphaFac = this.opaque ? 1 : 0; + + for (var y=0; y sh || scx < 0 || scx > sw) continue; + + var srcOff = (scy*sw+scx)*4; + var wt = weights[cy*side+cx]; + + r += src[srcOff] * wt; + g += src[srcOff+1] * wt; + b += src[srcOff+2] * wt; + a += src[srcOff+3] * wt; + } + } + dst[dstOff] = r; + dst[dstOff+1] = g; + dst[dstOff+2] = b; + dst[dstOff+3] = a + alphaFac*(255-a); + } + } + + context.putImageData(output, 0, 0); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + opaque: this.opaque, + matrix: this.matrix + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @return {fabric.Image.filters.Convolute} Instance of fabric.Image.filters.Convolute + */ + fabric.Image.filters.Convolute.fromObject = function(object) { + return new fabric.Image.filters.Convolute(object); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend; + + /** + * GradientTransparency filter class + * @class fabric.Image.filters.GradientTransparency + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @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)); + */ + fabric.Image.filters.GradientTransparency = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.GradientTransparency.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'GradientTransparency', + + /** + * Constructor + * @memberOf fabric.Image.filters.GradientTransparency.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 + * @return {fabric.Image.filters.GradientTransparency} Instance of fabric.Image.filters.GradientTransparency + */ + fabric.Image.filters.GradientTransparency.fromObject = function(object) { + return new fabric.Image.filters.GradientTransparency(object); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }); + + /** + * 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)); + */ + fabric.Image.filters.Grayscale = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Grayscale.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Grayscale', + + /** + * Applies filter to canvas element + * @memberOf fabric.Image.filters.Grayscale.prototype + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + len = imageData.width * imageData.height * 4, + index = 0, + average; + + while (index < len) { + average = (data[index] + data[index + 1] + data[index + 2]) / 3; + data[index] = average; + data[index + 1] = average; + data[index + 2] = average; + index += 4; + } + + context.putImageData(imageData, 0, 0); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @return {fabric.Image.filters.Grayscale} Instance of fabric.Image.filters.Grayscale + */ + fabric.Image.filters.Grayscale.fromObject = function() { + return new fabric.Image.filters.Grayscale(); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }); + + /** + * 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)); + */ + fabric.Image.filters.Invert = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Invert.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Invert', + + /** + * Applies filter to canvas element + * @memberOf fabric.Image.filters.Invert.prototype + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = data.length, i; + + for (i = 0; i < iLen; i+=4) { + data[i] = 255 - data[i]; + data[i + 1] = 255 - data[i + 1]; + data[i + 2] = 255 - data[i + 2]; + } + + context.putImageData(imageData, 0, 0); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @return {fabric.Image.filters.Invert} Instance of fabric.Image.filters.Invert + */ + fabric.Image.filters.Invert.fromObject = function() { + return new fabric.Image.filters.Invert(); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend; + + /** + * 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 + */ + fabric.Image.filters.Mask = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Mask.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Mask', + + /** + * Constructor + * @memberOf fabric.Image.filters.Mask.prototype + * @param {Object} [options] Options object + * @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 = maskEl.width; + maskCanvasEl.height = maskEl.height; + + maskCanvasEl.getContext('2d').drawImage(maskEl, 0, 0, maskEl.width, maskEl.height); + + var maskImageData = maskCanvasEl.getContext('2d').getImageData(0, 0, maskEl.width, maskEl.height), + maskData = maskImageData.data; + + for (i = 0; i < iLen; i += 4) { + data[i + 3] = maskData[i + channel]; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + mask: this.mask.toObject(), + channel: this.channel + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when a mask filter instance is created + */ + fabric.Image.filters.Mask.fromObject = function(object, callback) { + fabric.util.loadImage(object.mask.src, function(img) { + object.mask = new fabric.Image(img, object.mask); + callback && callback(new fabric.Image.filters.Mask(object)); + }); + }; + + /** + * 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; + + /** + * 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)); + */ + fabric.Image.filters.Noise = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Noise.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Noise', + + /** + * Constructor + * @memberOf fabric.Image.filters.Noise.prototype + * @param {Object} [options] Options object + * @param {Number} [options.noise=100] Noise value + */ + initialize: function(options) { + options = options || { }; + this.noise = options.noise || 100; + }, + + /** + * Applies filter to canvas element + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + noise = this.noise, rand; + + for (var i = 0, len = data.length; i < len; i += 4) { + + rand = (0.5 - Math.random()) * noise; + + data[i] += rand; + data[i + 1] += rand; + data[i + 2] += rand; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + noise: this.noise + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @return {fabric.Image.filters.Noise} Instance of fabric.Image.filters.Noise + */ + fabric.Image.filters.Noise.fromObject = function(object) { + return new fabric.Image.filters.Noise(object); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend; + + /** + * Pixelate filter class + * @class fabric.Image.filters.Pixelate + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @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)); + */ + fabric.Image.filters.Pixelate = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Pixelate.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Pixelate', + + /** + * Constructor + * @memberOf fabric.Image.filters.Pixelate.prototype + * @param {Object} [options] Options object + * @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 + * @return {fabric.Image.filters.Pixelate} Instance of fabric.Image.filters.Pixelate + */ + fabric.Image.filters.Pixelate.fromObject = function(object) { + return new fabric.Image.filters.Pixelate(object); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend; + + /** + * Remove white filter class + * @class fabric.Image.filters.RemoveWhite + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @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)); + */ + fabric.Image.filters.RemoveWhite = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.RemoveWhite.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'RemoveWhite', + + /** + * Constructor + * @memberOf fabric.Image.filters.RemoveWhite.prototype + * @param {Object} [options] Options object + * @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] = 1; + } + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + threshold: this.threshold, + distance: this.distance + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @return {fabric.Image.filters.RemoveWhite} Instance of fabric.Image.filters.RemoveWhite + */ + fabric.Image.filters.RemoveWhite.fromObject = function(object) { + return new fabric.Image.filters.RemoveWhite(object); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }); + + /** + * 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)); + */ + fabric.Image.filters.Sepia = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Sepia.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Sepia', + + /** + * Applies filter to canvas element + * @memberOf fabric.Image.filters.Sepia.prototype + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = data.length, i, avg; + + for (i = 0; i < iLen; i+=4) { + avg = 0.3 * data[i] + 0.59 * data[i + 1] + 0.11 * data[i + 2]; + data[i] = avg + 100; + data[i + 1] = avg + 50; + data[i + 2] = avg + 255; + } + + context.putImageData(imageData, 0, 0); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @return {fabric.Image.filters.Sepia} Instance of fabric.Image.filters.Sepia + */ + fabric.Image.filters.Sepia.fromObject = function() { + return new fabric.Image.filters.Sepia(); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }); + + /** + * 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)); + */ + fabric.Image.filters.Sepia2 = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Sepia2.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Sepia2', + + /** + * Applies filter to canvas element + * @memberOf fabric.Image.filters.Sepia.prototype + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = data.length, i, r, g, b; + + for (i = 0; i < iLen; i+=4) { + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + + data[i] = (r * 0.393 + g * 0.769 + b * 0.189 ) / 1.351; + data[i + 1] = (r * 0.349 + g * 0.686 + b * 0.168 ) / 1.203; + data[i + 2] = (r * 0.272 + g * 0.534 + b * 0.131 ) / 2.140; + } + + context.putImageData(imageData, 0, 0); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @return {fabric.Image.filters.Sepia2} Instance of fabric.Image.filters.Sepia2 + */ + fabric.Image.filters.Sepia2.fromObject = function() { + return new fabric.Image.filters.Sepia2(); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend; + + /** + * 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)); + */ + fabric.Image.filters.Tint = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Tint.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Tint', + + /** + * Constructor + * @memberOf fabric.Image.filters.Tint.prototype + * @param {Object} [options] Options object + * @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 + * @return {fabric.Image.filters.Tint} Instance of fabric.Image.filters.Tint + */ + fabric.Image.filters.Tint.fromObject = function(object) { + return new fabric.Image.filters.Tint(object); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + clone = fabric.util.object.clone, + toFixed = fabric.util.toFixed, + supportsLineDash = fabric.StaticCanvas.supports('setLineDash'); + + if (fabric.Text) { + fabric.warn('fabric.Text is already defined'); + return; + } + + var stateProperties = fabric.Object.prototype.stateProperties.concat(); + stateProperties.push( + 'fontFamily', + 'fontWeight', + 'fontSize', + 'text', + 'textDecoration', + 'textAlign', + 'fontStyle', + 'lineHeight', + 'textBackgroundColor', + 'useNative', + 'path' + ); + + /** + * 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: true, + fontWeight: true, + fontFamily: true, + textDecoration: true, + fontStyle: true, + lineHeight: true, + stroke: true, + strokeWidth: true, + text: true + }, + + /** + * @private + */ + _reNewline: /\r?\n/, + + /** + * Retrieves object's fontSize + * @method getFontSize + * @memberOf fabric.Text.prototype + * @return {String} Font size (in pixels) + */ + + /** + * Sets object's fontSize + * @method setFontSize + * @memberOf fabric.Text.prototype + * @param {Number} fontSize Font size (in pixels) + * @return {fabric.Text} + * @chainable + */ + + /** + * Retrieves object's fontWeight + * @method getFontWeight + * @memberOf fabric.Text.prototype + * @return {(String|Number)} Font weight + */ + + /** + * Sets object's fontWeight + * @method setFontWeight + * @memberOf fabric.Text.prototype + * @param {(Number|String)} fontWeight Font weight + * @return {fabric.Text} + * @chainable + */ + + /** + * Retrieves object's fontFamily + * @method getFontFamily + * @memberOf fabric.Text.prototype + * @return {String} Font family + */ + + /** + * Sets object's fontFamily + * @method setFontFamily + * @memberOf fabric.Text.prototype + * @param {String} fontFamily Font family + * @return {fabric.Text} + * @chainable + */ + + /** + * Retrieves object's text + * @method getText + * @memberOf fabric.Text.prototype + * @return {String} text + */ + + /** + * Sets object's text + * @method setText + * @memberOf fabric.Text.prototype + * @param {String} text Text + * @return {fabric.Text} + * @chainable + */ + + /** + * Retrieves object's textDecoration + * @method getTextDecoration + * @memberOf fabric.Text.prototype + * @return {String} Text decoration + */ + + /** + * Sets object's textDecoration + * @method setTextDecoration + * @memberOf fabric.Text.prototype + * @param {String} textDecoration Text decoration + * @return {fabric.Text} + * @chainable + */ + + /** + * Retrieves object's fontStyle + * @method getFontStyle + * @memberOf fabric.Text.prototype + * @return {String} Font style + */ + + /** + * Sets object's fontStyle + * @method setFontStyle + * @memberOf fabric.Text.prototype + * @param {String} fontStyle Font style + * @return {fabric.Text} + * @chainable + */ + + /** + * Retrieves object's lineHeight + * @method getLineHeight + * @memberOf fabric.Text.prototype + * @return {Number} Line height + */ + + /** + * Sets object's lineHeight + * @method setLineHeight + * @memberOf fabric.Text.prototype + * @param {Number} lineHeight Line height + * @return {fabric.Text} + * @chainable + */ + + /** + * Retrieves object's textAlign + * @method getTextAlign + * @memberOf fabric.Text.prototype + * @return {String} Text alignment + */ + + /** + * Sets object's textAlign + * @method setTextAlign + * @memberOf fabric.Text.prototype + * @param {String} textAlign Text alignment + * @return {fabric.Text} + * @chainable + */ + + /** + * Retrieves object's textBackgroundColor + * @method getTextBackgroundColor + * @memberOf fabric.Text.prototype + * @return {String} Text background color + */ + + /** + * Sets object's textBackgroundColor + * @method setTextBackgroundColor + * @memberOf fabric.Text.prototype + * @param {String} textBackgroundColor Text background color + * @return {fabric.Text} + * @chainable + */ + + /** + * Type of an object + * @type String + * @default + */ + type: 'text', + + /** + * Font size (in pixels) + * @type Number + * @default + */ + fontSize: 40, + + /** + * Font weight (e.g. bold, normal, 400, 600, 800) + * @type {(Number|String)} + * @default + */ + fontWeight: 'normal', + + /** + * Font family + * @type String + * @default + */ + fontFamily: 'Times New Roman', + + /** + * Text decoration Possible values: "", "underline", "overline" or "line-through". + * @type String + * @default + */ + textDecoration: '', + + /** + * Text alignment. Possible values: "left", "center", or "right". + * @type String + * @default + */ + textAlign: 'left', + + /** + * Font style . Possible values: "", "normal", "italic" or "oblique". + * @type String + * @default + */ + fontStyle: '', + + /** + * Line height + * @type Number + * @default + */ + lineHeight: 1.3, + + /** + * Background color of text lines + * @type String + * @default + */ + textBackgroundColor: '', + + /** + * URL of a font file, when using Cufon + * @type String | null + * @default + */ + path: null, + + /** + * Indicates whether canvas native text methods should be used to render text (otherwise, Cufon is used) + * @type Boolean + * @default + */ + useNative: true, + + /** + * List of properties to consider when checking if + * state of an object is changed ({@link fabric.Object#hasStateChanged}) + * as well as for history (undo/redo) purposes + * @type Array + */ + stateProperties: stateProperties, + + /** + * When defined, an object is rendered via stroke and this property specifies its color. + * Backwards incompatibility note: This property was named "strokeStyle" until v1.1.6 + * @type String + * @default + */ + stroke: null, + + /** + * Shadow object representing shadow of this shape. + * Backwards incompatibility note: This property was named "textShadow" (String) until v1.2.11 + * @type fabric.Shadow + * @default + */ + shadow: null, + + /** + * Constructor + * @param {String} text Text string + * @param {Object} [options] Options object + * @return {fabric.Text} thisArg + */ + initialize: function(text, options) { + options = options || { }; + + this.text = text; + this.__skipDimension = true; + this.setOptions(options); + this.__skipDimension = false; + this._initDimensions(); + this.setCoords(); + }, + + /** + * Renders text object on offscreen canvas, so that it would get dimensions + * @private + */ + _initDimensions: function() { + if (this.__skipDimension) return; + var canvasEl = fabric.util.createCanvasElement(); + this._render(canvasEl.getContext('2d')); + }, + + /** + * Returns string representation of an instance + * @return {String} String representation of text object + */ + toString: function() { + return '#'; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + + var isInPathGroup = this.group && this.group.type === 'path-group'; + if (isInPathGroup && !this.transformMatrix) { + ctx.translate(-this.group.width/2 + this.left, -this.group.height / 2 + this.top); + } + else if (isInPathGroup && this.transformMatrix) { + ctx.translate(-this.group.width/2, -this.group.height/2); + } + + if (typeof Cufon === 'undefined' || this.useNative === true) { + this._renderViaNative(ctx); + } + else { + this._renderViaCufon(ctx); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderViaNative: function(ctx) { + var textLines = this.text.split(this._reNewline); + + this.transform(ctx, fabric.isLikelyNode); + + this._setTextStyles(ctx); + + this.width = this._getTextWidth(ctx, textLines); + this.height = this._getTextHeight(ctx, textLines); + + this.clipTo && fabric.util.clipContext(this, ctx); + + this._renderTextBackground(ctx, textLines); + this._translateForTextAlign(ctx); + this._renderText(ctx, textLines); + + if (this.textAlign !== 'left' && this.textAlign !== 'justify') { + ctx.restore(); + } + + this._renderTextDecoration(ctx, textLines); + this.clipTo && ctx.restore(); + + this._setBoundaries(ctx, textLines); + this._totalLineHeight = 0; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderText: function(ctx, textLines) { + ctx.save(); + this._setShadow(ctx); + this._renderTextFill(ctx, textLines); + this._renderTextStroke(ctx, textLines); + this._removeShadow(ctx); + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _translateForTextAlign: function(ctx) { + if (this.textAlign !== 'left' && this.textAlign !== 'justify') { + ctx.save(); + ctx.translate(this.textAlign === 'center' ? (this.width / 2) : this.width, 0); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} textLines Array of all text lines + */ + _setBoundaries: function(ctx, textLines) { + this._boundaries = [ ]; + + for (var i = 0, len = textLines.length; i < len; i++) { + + var lineWidth = this._getLineWidth(ctx, textLines[i]); + var lineLeftOffset = this._getLineLeftOffset(lineWidth); + + this._boundaries.push({ + height: this.fontSize * this.lineHeight, + width: lineWidth, + left: lineLeftOffset + }); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _setTextStyles: function(ctx) { + this._setFillStyles(ctx); + this._setStrokeStyles(ctx); + ctx.textBaseline = 'alphabetic'; + if (!this.skipTextAlign) { + ctx.textAlign = this.textAlign; + } + ctx.font = this._getFontDeclaration(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} textLines Array of all text lines + * @return {Number} Height of fabric.Text object + */ + _getTextHeight: function(ctx, textLines) { + return this.fontSize * textLines.length * this.lineHeight; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} textLines Array of all text lines + * @return {Number} Maximum width of fabric.Text object + */ + _getTextWidth: function(ctx, textLines) { + var maxWidth = ctx.measureText(textLines[0] || '|').width; + + for (var i = 1, len = textLines.length; i < len; i++) { + var currentLineWidth = ctx.measureText(textLines[i]).width; + if (currentLineWidth > maxWidth) { + maxWidth = currentLineWidth; + } + } + return maxWidth; + }, + + /** + * @private + * @param {String} method Method name ("fillText" or "strokeText") + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} line Chars to render + * @param {Number} left Left position of text + * @param {Number} top Top position of text + */ + _renderChars: function(method, ctx, chars, left, top) { + ctx[method](chars, left, top); + }, + + /** + * @private + * @param {String} method Method name ("fillText" or "strokeText") + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} line Text to render + * @param {Number} left Left position of text + * @param {Number} top Top position of text + * @param {Number} lineIndex Index of a line in a text + */ + _renderTextLine: function(method, ctx, line, left, top, lineIndex) { + // lift the line by quarter of fontSize + top -= this.fontSize / 4; + + // short-circuit + if (this.textAlign !== 'justify') { + this._renderChars(method, ctx, line, left, top, lineIndex); + return; + } + + var lineWidth = ctx.measureText(line).width; + var totalWidth = this.width; + + if (totalWidth > lineWidth) { + // stretch the line + var words = line.split(/\s+/); + var wordsWidth = ctx.measureText(line.replace(/\s+/g, '')).width; + var widthDiff = totalWidth - wordsWidth; + var numSpaces = words.length - 1; + var spaceWidth = widthDiff / numSpaces; + + var leftOffset = 0; + for (var i = 0, len = words.length; i < len; i++) { + this._renderChars(method, ctx, words[i], left + leftOffset, top, lineIndex); + leftOffset += ctx.measureText(words[i]).width + spaceWidth; + } + } + else { + this._renderChars(method, ctx, line, left, top, lineIndex); + } + }, + + /** + * @private + * @return {Number} Left offset + */ + _getLeftOffset: function() { + if (fabric.isLikelyNode) { + return 0; + } + return -this.width / 2; + }, + + /** + * @private + * @return {Number} Top offset + */ + _getTopOffset: function() { + return -this.height / 2; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} textLines Array of all text lines + */ + _renderTextFill: function(ctx, textLines) { + if (!this.fill && !this._skipFillStrokeCheck) return; + + this._boundaries = [ ]; + var lineHeights = 0; + + for (var i = 0, len = textLines.length; i < len; i++) { + var heightOfLine = this._getHeightOfLine(ctx, i, textLines); + lineHeights += heightOfLine; + + this._renderTextLine( + 'fillText', + ctx, + textLines[i], + this._getLeftOffset(), + this._getTopOffset() + lineHeights, + i + ); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} textLines Array of all text lines + */ + _renderTextStroke: function(ctx, textLines) { + if (!this.stroke && !this._skipFillStrokeCheck) return; + + var lineHeights = 0; + + ctx.save(); + if (this.strokeDashArray) { + // Spec requires the concatenation of two copies the dash list when the number of elements is odd + if (1 & this.strokeDashArray.length) { + this.strokeDashArray.push.apply(this.strokeDashArray, this.strokeDashArray); + } + supportsLineDash && ctx.setLineDash(this.strokeDashArray); + } + + ctx.beginPath(); + for (var i = 0, len = textLines.length; i < len; i++) { + var heightOfLine = this._getHeightOfLine(ctx, i, textLines); + lineHeights += heightOfLine; + + this._renderTextLine( + 'strokeText', + ctx, + textLines[i], + this._getLeftOffset(), + this._getTopOffset() + lineHeights, + i + ); + } + ctx.closePath(); + ctx.restore(); + }, + + _getHeightOfLine: function() { + return this.fontSize * this.lineHeight; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} textLines Array of all text lines + */ + _renderTextBackground: function(ctx, textLines) { + this._renderTextBoxBackground(ctx); + this._renderTextLinesBackground(ctx, textLines); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextBoxBackground: function(ctx) { + if (!this.backgroundColor) return; + + ctx.save(); + ctx.fillStyle = this.backgroundColor; + + ctx.fillRect( + this._getLeftOffset(), + this._getTopOffset(), + this.width, + this.height + ); + + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} textLines Array of all text lines + */ + _renderTextLinesBackground: function(ctx, textLines) { + if (!this.textBackgroundColor) return; + + ctx.save(); + ctx.fillStyle = this.textBackgroundColor; + + for (var i = 0, len = textLines.length; i < len; i++) { + + if (textLines[i] !== '') { + + var lineWidth = this._getLineWidth(ctx, textLines[i]); + var lineLeftOffset = this._getLineLeftOffset(lineWidth); + + ctx.fillRect( + this._getLeftOffset() + lineLeftOffset, + this._getTopOffset() + (i * this.fontSize * this.lineHeight), + lineWidth, + this.fontSize * this.lineHeight + ); + } + } + ctx.restore(); + }, + + /** + * @private + * @param {Number} lineWidth Width of text line + * @return {Number} Line left offset + */ + _getLineLeftOffset: function(lineWidth) { + if (this.textAlign === 'center') { + return (this.width - lineWidth) / 2; + } + if (this.textAlign === 'right') { + return this.width - lineWidth; + } + return 0; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} line Text line + * @return {Number} Line width + */ + _getLineWidth: function(ctx, line) { + return this.textAlign === 'justify' + ? this.width + : ctx.measureText(line).width; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} textLines Array of all text lines + */ + _renderTextDecoration: function(ctx, textLines) { + if (!this.textDecoration) return; + + // var halfOfVerticalBox = this.originY === 'top' ? 0 : this._getTextHeight(ctx, textLines) / 2; + var halfOfVerticalBox = this._getTextHeight(ctx, textLines) / 2; + var _this = this; + + /** @ignore */ + function renderLinesAtOffset(offset) { + for (var i = 0, len = textLines.length; i < len; i++) { + + var lineWidth = _this._getLineWidth(ctx, textLines[i]); + var lineLeftOffset = _this._getLineLeftOffset(lineWidth); + + ctx.fillRect( + _this._getLeftOffset() + lineLeftOffset, + ~~((offset + (i * _this._getHeightOfLine(ctx, i, textLines))) - halfOfVerticalBox), + lineWidth, + 1); + } + } + + if (this.textDecoration.indexOf('underline') > -1) { + renderLinesAtOffset(this.fontSize * this.lineHeight); + } + if (this.textDecoration.indexOf('line-through') > -1) { + renderLinesAtOffset(this.fontSize * this.lineHeight - this.fontSize / 2); + } + if (this.textDecoration.indexOf('overline') > -1) { + renderLinesAtOffset(this.fontSize * this.lineHeight - this.fontSize); + } + }, + + /** + * @private + */ + _getFontDeclaration: function() { + return [ + // node-canvas needs "weight style", while browsers need "style weight" + (fabric.isLikelyNode ? this.fontWeight : this.fontStyle), + (fabric.isLikelyNode ? this.fontStyle : this.fontWeight), + this.fontSize + 'px', + (fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily) + ].join(' '); + }, + + /** + * Renders text instance on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Boolean} [noTransform] When true, context is not transformed + */ + render: function(ctx, noTransform) { + // do not render if object is not visible + if (!this.visible) return; + + ctx.save(); + var m = this.transformMatrix; + if (m && !this.group) { + ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + this._render(ctx); + if (!noTransform && this.active) { + this.drawBorders(ctx); + this.drawControls(ctx); + } + ctx.restore(); + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toObject: function(propertiesToInclude) { + var object = extend(this.callSuper('toObject', propertiesToInclude), { + text: this.text, + fontSize: this.fontSize, + fontWeight: this.fontWeight, + fontFamily: this.fontFamily, + fontStyle: this.fontStyle, + lineHeight: this.lineHeight, + textDecoration: this.textDecoration, + textAlign: this.textAlign, + path: this.path, + textBackgroundColor: this.textBackgroundColor, + useNative: this.useNative + }); + if (!this.includeDefaultValues) { + this._removeDefaultValues(object); + } + 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 = [ ], + textLines = this.text.split(this._reNewline), + offsets = this._getSVGLeftTopOffsets(textLines), + textAndBg = this._getSVGTextAndBg(offsets.lineTop, offsets.textLeft, textLines), + shadowSpans = this._getSVGShadows(offsets.lineTop, textLines); + + // move top offset by an ascent + offsets.textTop += (this._fontAscent ? ((this._fontAscent / 5) * this.lineHeight) : 0); + + this._wrapSVGTextAndBg(markup, textAndBg, shadowSpans, offsets); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + + /** + * @private + */ + _getSVGLeftTopOffsets: function(textLines) { + var lineTop = this.useNative + ? this.fontSize * this.lineHeight + : (-this._fontAscent - ((this._fontAscent / 5) * this.lineHeight)), + + textLeft = -(this.width/2), + textTop = this.useNative + ? this.fontSize - 1 + : (this.height/2) - (textLines.length * this.fontSize) - this._totalLineHeight; + + return { + textLeft: textLeft, + textTop: textTop, + lineTop: lineTop + }; + }, + + /** + * @private + */ + _wrapSVGTextAndBg: function(markup, textAndBg, shadowSpans, offsets) { + markup.push( + '', + textAndBg.textBgRects.join(''), + '', + shadowSpans.join(''), + textAndBg.textSpans.join(''), + '', + '' + ); + }, + + /** + * @private + * @param {Number} lineHeight + * @param {Array} textLines Array of all text lines + * @return {Array} + */ + _getSVGShadows: function(lineHeight, textLines) { + var shadowSpans = [], + i, len, + lineTopOffsetMultiplier = 1; + + if (!this.shadow || !this._boundaries) { + return shadowSpans; + } + + for (i = 0, len = textLines.length; i < len; i++) { + if (textLines[i] !== '') { + var lineLeftOffset = (this._boundaries && this._boundaries[i]) ? this._boundaries[i].left : 0; + shadowSpans.push( + '', + fabric.util.string.escapeXml(textLines[i]), + ''); + lineTopOffsetMultiplier = 1; + } + else { + // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier + // prevents empty tspans + lineTopOffsetMultiplier++; + } + } + + return shadowSpans; + }, + + /** + * @private + * @param {Number} lineHeight + * @param {Number} textLeftOffset Text left offset + * @param {Array} textLines Array of all text lines + * @return {Object} + */ + _getSVGTextAndBg: function(lineHeight, textLeftOffset, textLines) { + var textSpans = [ ], + textBgRects = [ ], + lineTopOffsetMultiplier = 1; + + // bounding-box background + this._setSVGBg(textBgRects); + + // text and text-background + for (var i = 0, len = textLines.length; i < len; i++) { + if (textLines[i] !== '') { + this._setSVGTextLineText(textLines[i], i, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects); + lineTopOffsetMultiplier = 1; + } + else { + // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier + // prevents empty tspans + lineTopOffsetMultiplier++; + } + + if (!this.textBackgroundColor || !this._boundaries) continue; + + this._setSVGTextLineBg(textBgRects, i, textLeftOffset, lineHeight); + } + + return { + textSpans: textSpans, + textBgRects: textBgRects + }; + }, + + _setSVGTextLineText: function(textLine, i, textSpans, lineHeight, lineTopOffsetMultiplier) { + var lineLeftOffset = (this._boundaries && this._boundaries[i]) + ? toFixed(this._boundaries[i].left, 2) + : 0; + + textSpans.push( + ' elements since setting opacity + // on containing one doesn't work in Illustrator + this._getFillAttributes(this.fill), '>', + fabric.util.string.escapeXml(textLine), + '' + ); + }, + + _setSVGTextLineBg: function(textBgRects, i, textLeftOffset, lineHeight) { + textBgRects.push( + ''); + }, + + _setSVGBg: function(textBgRects) { + if (this.backgroundColor && this._boundaries) { + textBgRects.push( + ''); + } + }, + + /** + * 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 {Any} 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 {Any} value + * @return {fabric.Text} thisArg + * @chainable + */ + _set: function(key, value) { + if (key === 'fontFamily' && this.path) { + this.path = this.path.replace(/(.*?)([^\/]*)(\.font\.js)/, '$1' + value + '$3'); + } + this.callSuper('_set', key, value); + + if (key in this._dimensionAffectingProps) { + this._initDimensions(); + this.setCoords(); + } + }, + + /** + * Returns complexity of an instance + * @return {Number} complexity + */ + complexity: function() { + return 1; + } + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Text.fromElement}) + * @static + * @memberOf fabric.Text + * @see: http://www.w3.org/TR/SVG/text.html#TextElement + */ + fabric.Text.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat( + 'x y font-family font-style font-weight font-size text-decoration'.split(' ')); + + /** + * Returns fabric.Text instance from an SVG element (not yet implemented) + * @static + * @memberOf fabric.Text + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @return {fabric.Text} Instance of fabric.Text + */ + fabric.Text.fromElement = function(element, options) { + if (!element) { + return null; + } + + var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES); + options = fabric.util.object.extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes); + + var text = new fabric.Text(element.textContent, options); + + /* + Adjust positioning: + x/y attributes in SVG correspond to the bottom-left corner of text bounding box + top/left properties in Fabric correspond to center point of text bounding box + */ + + text.set({ + left: text.getLeft() + text.getWidth() / 2, + top: text.getTop() - text.getHeight() / 2 + }); + + return text; + }; + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Text instance from an object representation + * @static + * @memberOf fabric.Text + * @param object {Object} object Object to create an instance from + * @return {fabric.Text} Instance of fabric.Text + */ + fabric.Text.fromObject = function(object) { + return new fabric.Text(object.text, clone(object)); + }; + + fabric.util.createAccessors(fabric.Text); + +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + + var clone = fabric.util.object.clone; + + /** + * IText class (introduced in v1.4) + * @class fabric.IText + * @extends fabric.Text + * @mixes fabric.Observable + * + * @fires changed ("text:changed" when observing canvas) + * @fires editing:entered ("text:editing:entered" when observing canvas) + * @fires editing:exited ("text:editing:exited" when observing canvas) + * + * @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
+    *   Select till start/end of line:  cmd + shift + left, cmd + shift + right
+    *   Jump to start/end of text:      cmd + up, cmd + down
+    *   Select till start/end of text:  cmd + shift + up, cmd + shift + down
+    *   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
+    * 
+ * + *

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 Nubmer + * @default + */ + selectionStart: 0, + + /** + * Index where text selection ends + * @type Nubmer + * @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 + * @type Boolean + * @default + */ + _skipFillStrokeCheck: true, + + /** + * @private + */ + _reSpace: /\s|\n/, + + /** + * @private + */ + _fontSizeFraction: 4, + + /** + * @private + */ + _currentCursorOpacity: 0, + + /** + * @private + */ + _selectionDirection: null, + + /** + * @private + */ + _abortCursorAnimation: false, + + /** + * @private + */ + _charWidthsCache: { }, + + /** + * 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(); + + fabric.IText.instances.push(this); + + // caching + this.__lineWidths = { }; + this.__lineHeights = { }; + this.__lineOffsets = { }; + }, + + /** + * 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]) { + /*jshint unused:false */ + 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) { + this.selectionStart = index; + this.hiddenTextarea && (this.hiddenTextarea.selectionStart = index); + }, + + /** + * Sets selection end (right boundary of a selection) + * @param {Number} index Index to set selection end to + */ + setSelectionEnd: function(index) { + this.selectionEnd = index; + this.hiddenTextarea && (this.hiddenTextarea.selectionEnd = index); + }, + + /** + * 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); + if (this.styles[loc.lineIndex]) { + return this.styles[loc.lineIndex][loc.charIndex] || { }; + } + + return { }; + }, + + /** + * 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); + } + } + return this; + }, + + /** + * @private + */ + _extendStyles: function(index, styles) { + var loc = this.get2DCursorLocation(index); + + if (!this.styles[loc.lineIndex]) { + this.styles[loc.lineIndex] = { }; + } + if (!this.styles[loc.lineIndex][loc.charIndex]) { + this.styles[loc.lineIndex][loc.charIndex] = { }; + } + + fabric.util.object.extend(this.styles[loc.lineIndex][loc.charIndex], styles); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + this.callSuper('_render', ctx); + this.ctx = ctx; + this.isEditing && this.renderCursorOrSelection(); + }, + + /** + * Renders cursor or selection (depending on what exists) + */ + renderCursorOrSelection: function() { + if (!this.active) return; + + var chars = this.text.split(''), + boundaries; + + if (this.selectionStart === this.selectionEnd) { + boundaries = this._getCursorBoundaries(chars, 'cursor'); + this.renderCursor(boundaries); + } + else { + boundaries = this._getCursorBoundaries(chars, 'selection'); + this.renderSelection(chars, boundaries); + } + }, + + /** + * 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 textBeforeCursor = this.text.slice(0, selectionStart); + var linesBeforeCursor = textBeforeCursor.split(this._reNewline); + + return { + lineIndex: linesBeforeCursor.length - 1, + charIndex: linesBeforeCursor[linesBeforeCursor.length - 1].length + }; + }, + + /** + * 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) { + return ( + this.styles[lineIndex] && + this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)] && + this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)].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) { + return ( + this.styles[lineIndex] && + this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)] && + this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)].fill) || this.cursorColor; + }, + + /** + * Returns cursor boundaries (left, top, leftOffset, topOffset) + * @private + * @param {Array} chars Array of characters + * @param {String} typeOfBoundaries + */ + _getCursorBoundaries: function(chars, typeOfBoundaries) { + + var cursorLocation = this.get2DCursorLocation(), + + textLines = this.text.split(this._reNewline), + + // left/top are left/top of entire text box + // leftOffset/topOffset are offset from that left/top point of a text box + + left = Math.round(this._getLeftOffset()), + top = -this.height / 2, + + offsets = this._getCursorBoundariesOffsets( + chars, typeOfBoundaries, cursorLocation, textLines); + + return { + left: left, + top: top, + leftOffset: offsets.left + offsets.lineLeft, + topOffset: offsets.top + }; + }, + + /** + * @private + */ + _getCursorBoundariesOffsets: function(chars, typeOfBoundaries, cursorLocation, textLines) { + + var lineLeftOffset = 0, + + lineIndex = 0, + charIndex = 0, + + leftOffset = 0, + topOffset = typeOfBoundaries === 'cursor' + // selection starts at the very top of the line, + // whereas cursor starts at the padding created by line height + ? (this._getHeightOfLine(this.ctx, 0) - + this.getCurrentCharFontSize(cursorLocation.lineIndex, cursorLocation.charIndex)) + : 0; + + for (var i = 0; i < this.selectionStart; i++) { + if (chars[i] === '\n') { + leftOffset = 0; + var index = lineIndex + (typeOfBoundaries === 'cursor' ? 1 : 0); + topOffset += this._getCachedLineHeight(index); + + lineIndex++; + charIndex = 0; + } + else { + leftOffset += this._getWidthOfChar(this.ctx, chars[i], lineIndex, charIndex); + charIndex++; + } + + lineLeftOffset = this._getCachedLineOffset(lineIndex, textLines); + } + + this._clearCache(); + + return { + top: topOffset, + left: leftOffset, + lineLeft: lineLeftOffset + }; + }, + + /** + * @private + */ + _clearCache: function() { + this.__lineWidths = { }; + this.__lineHeights = { }; + this.__lineOffsets = { }; + }, + + /** + * @private + */ + _getCachedLineHeight: function(index) { + return this.__lineHeights[index] || + (this.__lineHeights[index] = this._getHeightOfLine(this.ctx, index)); + }, + + /** + * @private + */ + _getCachedLineWidth: function(lineIndex, textLines) { + return this.__lineWidths[lineIndex] || + (this.__lineWidths[lineIndex] = this._getWidthOfLine(this.ctx, lineIndex, textLines)); + }, + + /** + * @private + */ + _getCachedLineOffset: function(lineIndex, textLines) { + var widthOfLine = this._getCachedLineWidth(lineIndex, textLines); + + return this.__lineOffsets[lineIndex] || + (this.__lineOffsets[lineIndex] = this._getLineLeftOffset(widthOfLine)); + }, + + /** + * Renders cursor + * @param {Object} boundaries + */ + renderCursor: function(boundaries) { + var ctx = this.ctx; + + ctx.save(); + + var cursorLocation = this.get2DCursorLocation(), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex, + charHeight = this.getCurrentCharFontSize(lineIndex, charIndex), + leftOffset = (lineIndex === 0 && charIndex === 0) + ? this._getCachedLineOffset(lineIndex, this.text.split(this._reNewline)) + : boundaries.leftOffset; + + ctx.fillStyle = this.getCurrentCharColor(lineIndex, charIndex); + ctx.globalAlpha = this.__isMousedown ? 1 : this._currentCursorOpacity; + + ctx.fillRect( + boundaries.left + leftOffset, + boundaries.top + boundaries.topOffset, + this.cursorWidth / this.scaleX, + charHeight); + + ctx.restore(); + }, + + /** + * Renders text selection + * @param {Array} chars Array of characters + * @param {Object} boundaries Object with left/top/leftOffset/topOffset + */ + renderSelection: function(chars, boundaries) { + var ctx = this.ctx; + + ctx.save(); + + ctx.fillStyle = this.selectionColor; + + var start = this.get2DCursorLocation(this.selectionStart), + end = this.get2DCursorLocation(this.selectionEnd), + startLine = start.lineIndex, + endLine = end.lineIndex, + textLines = this.text.split(this._reNewline), + charIndex = start.charIndex - textLines[0].length; + + for (var i = startLine; i <= endLine; i++) { + var lineOffset = this._getCachedLineOffset(i, textLines) || 0, + lineHeight = this._getCachedLineHeight(i), + boxWidth = 0; + + if (i === startLine) { + for (var j = 0, len = textLines[i].length; j < len; j++) { + if (j >= start.charIndex && (i !== endLine || j < end.charIndex)) { + boxWidth += this._getWidthOfChar(ctx, textLines[i][j], i, charIndex); + } + if (j < start.charIndex) { + lineOffset += this._getWidthOfChar(ctx, textLines[i][j], i, charIndex); + } + charIndex++; + } + } + else if (i > startLine && i < endLine) { + boxWidth += this._getCachedLineWidth(i, textLines) || 5; + charIndex += textLines[i].length; + } + else if (i === endLine) { + for (var j2 = 0, j2len = end.charIndex; j2 < j2len; j2++) { + boxWidth += this._getWidthOfChar(ctx, textLines[i][j2], i, charIndex); + charIndex++; + } + } + + ctx.fillRect( + boundaries.left + lineOffset, + boundaries.top + boundaries.topOffset, + boxWidth, + lineHeight); + + boundaries.topOffset += lineHeight; + } + ctx.restore(); + }, + + /** + * @private + * @param {String} method + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderChars: function(method, ctx, line, left, top, lineIndex) { + + if (this.isEmptyStyles()) { + return this._renderCharsFast(method, ctx, line, left, top); + } + + this.skipTextAlign = true; + + // set proper box offset + left -= this.textAlign === 'center' + ? (this.width / 2) + : (this.textAlign === 'right') + ? this.width + : 0; + + // set proper line offset + var textLines = this.text.split(this._reNewline), + lineWidth = this._getWidthOfLine(ctx, lineIndex, textLines), + lineHeight = this._getHeightOfLine(ctx, lineIndex, textLines), + lineLeftOffset = this._getLineLeftOffset(lineWidth), + chars = line.split(''); + + left += lineLeftOffset || 0; + + ctx.save(); + for (var i = 0, len = chars.length; i < len; i++) { + this._renderChar(method, ctx, lineIndex, i, chars[i], left, top, lineHeight); + } + ctx.restore(); + }, + + /** + * @private + * @param {String} method + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} line + */ + _renderCharsFast: function(method, ctx, line, left, top) { + this.skipTextAlign = false; + + if (method === 'fillText' && this.fill) { + this.callSuper('_renderChars', method, ctx, line, left, top); + } + if (method === 'strokeText' && this.stroke) { + this.callSuper('_renderChars', method, ctx, line, left, top); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderChar: function(method, ctx, lineIndex, i, _char, left, top, lineHeight) { + var decl, charWidth, charHeight; + + if (this.styles && this.styles[lineIndex] && (decl = this.styles[lineIndex][i])) { + + var shouldStroke = decl.stroke || this.stroke, + shouldFill = decl.fill || this.fill; + + ctx.save(); + charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i, decl); + charHeight = this._getHeightOfChar(ctx, _char, lineIndex, i); + + if (shouldFill) { + ctx.fillText(_char, left, top); + } + if (shouldStroke) { + ctx.strokeText(_char, left, top); + } + + this._renderCharDecoration(ctx, decl, left, top, charWidth, lineHeight, charHeight); + ctx.restore(); + + ctx.translate(charWidth, 0); + } + else { + if (method === 'strokeText' && this.stroke) { + ctx[method](_char, left, top); + } + if (method === 'fillText' && this.fill) { + ctx[method](_char, left, top); + } + charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i); + this._renderCharDecoration(ctx, null, left, top, charWidth, lineHeight); + + ctx.translate(ctx.measureText(_char).width, 0); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderCharDecoration: function(ctx, styleDeclaration, left, top, charWidth, lineHeight, charHeight) { + + var textDecoration = styleDeclaration + ? (styleDeclaration.textDecoration || this.textDecoration) + : this.textDecoration; + + var fontSize = (styleDeclaration ? styleDeclaration.fontSize : null) || this.fontSize; + + if (!textDecoration) return; + + if (textDecoration.indexOf('underline') > -1) { + this._renderCharDecorationAtOffset( + ctx, + left, + top + (this.fontSize / this._fontSizeFraction), + charWidth, + 0, + this.fontSize / 20 + ); + } + if (textDecoration.indexOf('line-through') > -1) { + this._renderCharDecorationAtOffset( + ctx, + left, + top + (this.fontSize / this._fontSizeFraction), + charWidth, + charHeight / 2, + fontSize / 20 + ); + } + if (textDecoration.indexOf('overline') > -1) { + this._renderCharDecorationAtOffset( + ctx, + left, + top, + charWidth, + lineHeight - (this.fontSize / this._fontSizeFraction), + this.fontSize / 20 + ); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderCharDecorationAtOffset: function(ctx, left, top, charWidth, offset, thickness) { + ctx.fillRect(left, top - offset, charWidth, thickness); + }, + + /** + * @private + * @param {String} method + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} line + */ + _renderTextLine: function(method, ctx, line, left, top, lineIndex) { + // to "cancel" this.fontSize subtraction in fabric.Text#_renderTextLine + top += this.fontSize / 4; + this.callSuper('_renderTextLine', method, ctx, line, left, top, lineIndex); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} textLines + */ + _renderTextDecoration: function(ctx, textLines) { + if (this.isEmptyStyles()) { + return this.callSuper('_renderTextDecoration', ctx, textLines); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} textLines Array of all text lines + */ + _renderTextLinesBackground: function(ctx, textLines) { + if (!this.textBackgroundColor && !this.styles) return; + + ctx.save(); + + if (this.textBackgroundColor) { + ctx.fillStyle = this.textBackgroundColor; + } + + var lineHeights = 0, + fractionOfFontSize = this.fontSize / this._fontSizeFraction; + + for (var i = 0, len = textLines.length; i < len; i++) { + + var heightOfLine = this._getHeightOfLine(ctx, i, textLines); + if (textLines[i] === '') { + lineHeights += heightOfLine; + continue; + } + + var lineWidth = this._getWidthOfLine(ctx, i, textLines), + lineLeftOffset = this._getLineLeftOffset(lineWidth); + + if (this.textBackgroundColor) { + ctx.fillStyle = this.textBackgroundColor; + + ctx.fillRect( + this._getLeftOffset() + lineLeftOffset, + this._getTopOffset() + lineHeights + fractionOfFontSize, + lineWidth, + heightOfLine + ); + } + if (this.styles[i]) { + for (var j = 0, jlen = textLines[i].length; j < jlen; j++) { + if (this.styles[i] && this.styles[i][j] && this.styles[i][j].textBackgroundColor) { + + var _char = textLines[i][j]; + + ctx.fillStyle = this.styles[i][j].textBackgroundColor; + + ctx.fillRect( + this._getLeftOffset() + lineLeftOffset + this._getWidthOfCharsAt(ctx, i, j, textLines), + this._getTopOffset() + lineHeights + fractionOfFontSize, + this._getWidthOfChar(ctx, _char, i, j, textLines) + 1, + heightOfLine + ); + } + } + } + lineHeights += heightOfLine; + } + ctx.restore(); + }, + + /** + * @private + */ + _getCacheProp: function(_char, styleDeclaration) { + return _char + + + styleDeclaration.fontFamily + + styleDeclaration.fontSize + + styleDeclaration.fontWeight + + styleDeclaration.fontStyle + + + styleDeclaration.shadow; + }, + + /** + * @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 styleDeclaration = decl || + (this.styles[lineIndex] && + this.styles[lineIndex][charIndex]); + + if (styleDeclaration) { + // cloning so that original style object is not polluted with following font declarations + styleDeclaration = clone(styleDeclaration); + } + else { + styleDeclaration = { }; + } + + this._applyFontStyles(styleDeclaration); + + var cacheProp = this._getCacheProp(_char, styleDeclaration); + + // short-circuit if no styles + if (this.isEmptyStyles() && this._charWidthsCache[cacheProp] && this.caching) { + return this._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) + : fill; + + if (styleDeclaration.stroke) { + ctx.strokeStyle = (styleDeclaration.stroke && styleDeclaration.stroke.toLive) + ? styleDeclaration.stroke.toLive(ctx) + : styleDeclaration.stroke; + } + + ctx.lineWidth = styleDeclaration.strokeWidth || this.strokeWidth; + ctx.font = this._getFontDeclaration.call(styleDeclaration); + this._setShadow.call(styleDeclaration, ctx); + + if (!this.caching) { + return ctx.measureText(_char).width; + } + + if (!this._charWidthsCache[cacheProp]) { + this._charWidthsCache[cacheProp] = ctx.measureText(_char).width; + } + + return this._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; + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getWidthOfChar: function(ctx, _char, lineIndex, charIndex) { + ctx.save(); + var width = this._applyCharStylesGetWidth(ctx, _char, lineIndex, charIndex); + ctx.restore(); + return width; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getHeightOfChar: function(ctx, _char, lineIndex, charIndex) { + if (this.styles[lineIndex] && this.styles[lineIndex][charIndex]) { + return this.styles[lineIndex][charIndex].fontSize || this.fontSize; + } + return this.fontSize; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getWidthOfCharAt: function(ctx, lineIndex, charIndex, lines) { + lines = lines || this.text.split(this._reNewline); + var _char = lines[lineIndex].split('')[charIndex]; + return this._getWidthOfChar(ctx, _char, lineIndex, charIndex); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getHeightOfCharAt: function(ctx, lineIndex, charIndex, lines) { + lines = lines || this.text.split(this._reNewline); + var _char = lines[lineIndex].split('')[charIndex]; + return this._getHeightOfChar(ctx, _char, lineIndex, charIndex); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getWidthOfCharsAt: function(ctx, lineIndex, charIndex, lines) { + var width = 0; + for (var i = 0; i < charIndex; i++) { + width += this._getWidthOfCharAt(ctx, lineIndex, i, lines); + } + return width; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getWidthOfLine: function(ctx, lineIndex, textLines) { + // if (!this.styles[lineIndex]) { + // return this.callSuper('_getLineWidth', ctx, textLines[lineIndex]); + // } + return this._getWidthOfCharsAt(ctx, lineIndex, textLines[lineIndex].length, textLines); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getTextWidth: function(ctx, textLines) { + + if (this.isEmptyStyles()) { + return this.callSuper('_getTextWidth', ctx, textLines); + } + + var maxWidth = this._getWidthOfLine(ctx, 0, textLines); + + for (var i = 1, len = textLines.length; i < len; i++) { + var currentLineWidth = this._getWidthOfLine(ctx, i, textLines); + if (currentLineWidth > maxWidth) { + maxWidth = currentLineWidth; + } + } + return maxWidth; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getHeightOfLine: function(ctx, lineIndex, textLines) { + + textLines = textLines || this.text.split(this._reNewline); + + var maxHeight = this._getHeightOfChar(ctx, textLines[lineIndex][0], lineIndex, 0), + line = textLines[lineIndex], + chars = line.split(''); + + for (var i = 1, len = chars.length; i < len; i++) { + var currentCharHeight = this._getHeightOfChar(ctx, chars[i], lineIndex, i); + if (currentCharHeight > maxHeight) { + maxHeight = currentCharHeight; + } + } + + return maxHeight * this.lineHeight; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getTextHeight: function(ctx, textLines) { + var height = 0; + for (var i = 0, len = textLines.length; i < len; i++) { + height += this._getHeightOfLine(ctx, i, textLines); + } + return height; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getTopOffset: function() { + var topOffset = fabric.Text.prototype._getTopOffset.call(this); + return topOffset - (this.fontSize / this._fontSizeFraction); + }, + + /** + * @private + * This method is overwritten to account for different top offset + */ + _renderTextBoxBackground: function(ctx) { + if (!this.backgroundColor) return; + + ctx.save(); + ctx.fillStyle = this.backgroundColor; + + ctx.fillRect( + this._getLeftOffset(), + this._getTopOffset() + (this.fontSize / this._fontSizeFraction), + this.width, + this.height + ); + + ctx.restore(); + }, + + /** + * 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 fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), { + styles: clone(this.styles) + }); + } + }); + + /** + * Returns fabric.IText instance from an object representation + * @static + * @memberOf fabric.IText + * @param {Object} object Object to create an instance from + * @return {fabric.IText} instance of fabric.IText + */ + fabric.IText.fromObject = function(object) { + return new fabric.IText(object.text, clone(object)); + }; + + /** + * Contains all fabric.IText objects that have been created + * @static + * @memberof fabric.IText + * @type Array + */ + fabric.IText.instances = [ ]; + +})(); + + +(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.initKeyHandlers(); + this.initCursorSelectionHandlers(); + this.initDoubleClickSimulation(); + this.initHiddenTextarea(); + }, + + /** + * Initializes "selected" event handler + */ + initSelectedHandler: function() { + this.on('selected', function() { + + var _this = this; + setTimeout(function() { + _this.selected = true; + }, 100); + + if (this.canvas && !this.canvas._hasITextHandlers) { + this._initCanvasHandlers(); + this.canvas._hasITextHandlers = true; + } + }); + }, + + /** + * @private + */ + _initCanvasHandlers: function() { + this.canvas.on('selection:cleared', function() { + fabric.IText.prototype.exitEditingOnOthers.call(); + }); + + this.canvas.on('mouse:up', function() { + fabric.IText.instances.forEach(function(obj) { + obj.__isMousedown = false; + }); + }); + + this.canvas.on('object:selected', function(options) { + fabric.IText.prototype.exitEditingOnOthers.call(options.target); + }); + }, + + /** + * @private + */ + _tick: function() { + + var _this = this; + + if (this._abortCursorAnimation) return; + + this.animate('_currentCursorOpacity', 1, { + + duration: this.cursorDuration, + + onComplete: function() { + _this._onTickComplete(); + }, + + onChange: function() { + _this.canvas && _this.canvas.renderAll(); + }, + + abort: function() { + return _this._abortCursorAnimation; + } + }); + }, + + /** + * @private + */ + _onTickComplete: function() { + if (this._abortCursorAnimation) return; + + var _this = this; + if (this._cursorTimeout1) { + clearTimeout(this._cursorTimeout1); + } + this._cursorTimeout1 = setTimeout(function() { + _this.animate('_currentCursorOpacity', 0, { + duration: this.cursorDuration / 2, + onComplete: function() { + _this._tick(); + }, + onChange: function() { + _this.canvas && _this.canvas.renderAll(); + }, + abort: function() { + return _this._abortCursorAnimation; + } + }); + }, 100); + }, + + /** + * Initializes delayed cursor + */ + initDelayedCursor: function(restart) { + var _this = this; + var delay = restart ? 0 : this.cursorDelay; + + if (restart) { + this._abortCursorAnimation = true; + clearTimeout(this._cursorTimeout1); + this._currentCursorOpacity = 1; + this.canvas && this.canvas.renderAll(); + } + if (this._cursorTimeout2) { + clearTimeout(this._cursorTimeout2); + } + this._cursorTimeout2 = setTimeout(function() { + _this._abortCursorAnimation = false; + _this._tick(); + }, delay); + }, + + /** + * Aborts cursor animation and clears all timeouts + */ + abortCursorAnimation: function() { + this._abortCursorAnimation = true; + + clearTimeout(this._cursorTimeout1); + clearTimeout(this._cursorTimeout2); + + this._currentCursorOpacity = 0; + this.canvas && this.canvas.renderAll(); + + var _this = this; + setTimeout(function() { + _this._abortCursorAnimation = false; + }, 10); + }, + + /** + * Selects entire text + */ + selectAll: function() { + this.selectionStart = 0; + this.selectionEnd = this.text.length; + }, + + /** + * 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} current 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} current 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(); + var numNewLines = 0; + for (var i = 0, chars = selectedText.split(''), len = chars.length; i < len; i++) { + if (chars[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 + */ + searchWordBoundary: function(selectionStart, direction) { + var index = this._reSpace.test(this.text.charAt(selectionStart)) ? selectionStart-1 : selectionStart; + var _char = this.text.charAt(index); + var 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) { + var newSelectionStart = this.searchWordBoundary(selectionStart, -1); /* search backwards */ + var newSelectionEnd = this.searchWordBoundary(selectionStart, 1); /* search forward */ + + this.setSelectionStart(newSelectionStart); + this.setSelectionEnd(newSelectionEnd); + this.initDelayedCursor(true); + }, + + /** + * Selects a line based on the index + * @param {Number} selectionStart Index of a character + */ + selectLine: function(selectionStart) { + var newSelectionStart = this.findLineBoundaryLeft(selectionStart); + var newSelectionEnd = this.findLineBoundaryRight(selectionStart); + + this.setSelectionStart(newSelectionStart); + this.setSelectionEnd(newSelectionEnd); + this.initDelayedCursor(true); + }, + + /** + * Enters editing state + * @return {fabric.IText} thisArg + * @chainable + */ + enterEditing: function() { + if (this.isEditing || !this.editable) return; + + this.exitEditingOnOthers(); + + this.isEditing = true; + + this._updateTextarea(); + this._saveEditingProps(); + this._setEditingProps(); + + this._tick(); + this.canvas && this.canvas.renderAll(); + + this.fire('editing:entered'); + this.canvas && this.canvas.fire('text:editing:entered', { target: this }); + + return this; + }, + + exitEditingOnOthers: function() { + fabric.IText.instances.forEach(function(obj) { + if (obj === this) return; + obj.exitEditing(); + }, this); + }, + + /** + * @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) return; + + this.hiddenTextarea.value = this.text; + this.hiddenTextarea.selectionStart = this.selectionStart; + this.hiddenTextarea.focus(); + }, + + /** + * @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() { + + this.selected = false; + this.isEditing = false; + this.selectable = true; + + this.selectionEnd = this.selectionStart; + this.hiddenTextarea && this.hiddenTextarea.blur(); + + this.abortCursorAnimation(); + this._restoreEditingProps(); + this._currentCursorOpacity = 0; + + this.fire('editing:exited'); + this.canvas && this.canvas.fire('text:editing:exited', { target: this }); + + return this; + }, + + /** + * @private + */ + _removeExtraneousStyles: function() { + var textLines = this.text.split(this._reNewline); + for (var prop in this.styles) { + if (!textLines[prop]) { + delete this.styles[prop]; + } + } + }, + + /** + * @private + */ + _removeCharsFromTo: function(start, end) { + + var i = end; + while (i !== start) { + + var prevIndex = this.get2DCursorLocation(i).charIndex; + i--; + var index = this.get2DCursorLocation(i).charIndex; + var isNewline = index > prevIndex; + + if (isNewline) { + this.removeStyleObject(isNewline, i + 1); + } + else { + this.removeStyleObject(this.get2DCursorLocation(i).charIndex === 0, i); + } + + } + + this.text = this.text.slice(0, start) + + this.text.slice(end); + }, + + /** + * Inserts a character where cursor is (replacing selection if one exists) + * @param {String} _chars Characters to insert + */ + insertChars: function(_chars) { + var isEndOfLine = this.text.slice(this.selectionStart, this.selectionStart + 1) === '\n'; + + this.text = this.text.slice(0, this.selectionStart) + + _chars + + this.text.slice(this.selectionEnd); + + if (this.selectionStart === this.selectionEnd) { + this.insertStyleObjects(_chars, isEndOfLine, this.copiedStyles); + } + else if (this.selectionEnd - this.selectionStart > 1) { + // TODO: replace styles properly + // console.log('replacing MORE than 1 char'); + } + + this.selectionStart += _chars.length; + this.selectionEnd = this.selectionStart; + + if (this.canvas) { + // TODO: double renderAll gets rid of text box shift happenning sometimes + // need to find out what exactly causes it and fix it + this.canvas.renderAll().renderAll(); + } + + this.setCoords(); + this.fire('changed'); + this.canvas && this.canvas.fire('text:changed', { target: this }); + }, + + /** + * 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); + + if (!this.styles[lineIndex + 1]) { + this.styles[lineIndex + 1] = { }; + } + + var currentCharStyle = this.styles[lineIndex][charIndex - 1], + newLineStyles = { }; + + // if there's nothing after cursor, + // we clone current char style onto the next (otherwise empty) line + if (isEndOfLine) { + 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 { + for (var index in this.styles[lineIndex]) { + if (parseInt(index, 10) >= charIndex) { + newLineStyles[parseInt(index, 10) - charIndex] = this.styles[lineIndex][index]; + // remove lines from the previous line since they're on a new line now + delete this.styles[lineIndex][index]; + } + } + this.styles[lineIndex + 1] = newLineStyles; + } + }, + + /** + * 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]; + //delete currentLineStyles[index]; + } + } + + this.styles[lineIndex][charIndex] = + style || clone(currentLineStyles[charIndex - 1]); + }, + + /** + * 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 {Array} [styles] Styles to insert + */ + insertStyleObjects: function(_chars, isEndOfLine, styles) { + + // short-circuit + if (this.isEmptyStyles()) return; + + var cursorLocation = this.get2DCursorLocation(), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex; + + if (!this.styles[lineIndex]) { + this.styles[lineIndex] = { }; + } + + if (_chars === '\n') { + this.insertNewlineStyleObject(lineIndex, charIndex, isEndOfLine); + } + else { + if (styles) { + this._insertStyles(styles); + } + else { + // TODO: support multiple style insertion if _chars.length > 1 + this.insertCharStyleObject(lineIndex, charIndex); + } + } + }, + + /** + * @private + */ + _insertStyles: function(styles) { + for (var i = 0, len = styles.length; i < len; i++) { + + var cursorLocation = this.get2DCursorLocation(this.selectionStart + i), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex; + + this.insertCharStyleObject(lineIndex, charIndex, styles[i]); + } + }, + + /** + * 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 + var clonedStyles = clone(this.styles); + for (var line in this.styles) { + var numericLine = parseInt(line, 10); + if (numericLine > lineIndex) { + this.styles[numericLine + offset] = clonedStyles[numericLine]; + } + } + }, + + /** + * 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; + + if (isBeginningOfLine) { + + var textLines = this.text.split(this._reNewline), + textOnPreviousLine = textLines[lineIndex - 1], + 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(lineIndex, -1); + } + else { + var currentLineStyles = this.styles[lineIndex]; + + if (currentLineStyles) { + var offset = this.selectionStart === this.selectionEnd ? -1 : 0; + delete currentLineStyles[charIndex + offset]; + // console.log('deleting', lineIndex, charIndex + offset); + } + + 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'); + } + }); +})(); + + +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; + }, + + 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.initSelectedHandler(); + this.initMousedownHandler(); + this.initMousemoveHandler(); + 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) { + + var pointer = this.canvas.getPointer(options.e); + + this.__mousedownX = pointer.x; + this.__mousedownY = pointer.y; + this.__isMousedown = true; + + if (this.hiddenTextarea && this.canvas) { + this.canvas.wrapperEl.appendChild(this.hiddenTextarea); + } + + if (this.selected) { + this.setCursorByClick(options.e); + } + + if (this.isEditing) { + this.__selectionStartOnMouseDown = this.selectionStart; + this.initDelayedCursor(true); + } + }); + }, + + /** + * Initializes "mousemove" event handler + */ + initMousemoveHandler: function() { + this.on('mousemove', function(options) { + if (!this.__isMousedown || !this.isEditing) return; + + var newSelectionStart = this.getSelectionStartFromPointer(options.e); + + if (newSelectionStart >= this.__selectionStartOnMouseDown) { + this.setSelectionStart(this.__selectionStartOnMouseDown); + this.setSelectionEnd(newSelectionStart); + } + else { + this.setSelectionStart(newSelectionStart); + this.setSelectionEnd(this.__selectionStartOnMouseDown); + } + }); + }, + + /** + * @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._isObjectMoved(options.e)) return; + + if (this.selected) { + this.enterEditing(); + this.initDelayedCursor(true); + } + }); + }, + + /** + * Changes cursor location in a text depending on passed pointer (x/y) object + * @param {Object} pointer Pointer object with x and y numeric properties + */ + setCursorByClick: function(e) { + var newSelectionStart = this.getSelectionStartFromPointer(e); + + if (e.shiftKey) { + if (newSelectionStart < this.selectionStart) { + this.setSelectionEnd(this.selectionStart); + this.setSelectionStart(newSelectionStart); + } + else { + this.setSelectionEnd(newSelectionStart); + } + } + else { + this.setSelectionStart(newSelectionStart); + this.setSelectionEnd(newSelectionStart); + } + }, + + /** + * @private + * @param {Event} e Event object + * @param {Object} Object with x/y corresponding to local offset (according to object rotation) + */ + _getLocalRotatedPointer: function(e) { + var pointer = this.canvas.getPointer(e), + + pClicked = new fabric.Point(pointer.x, pointer.y), + pLeftTop = new fabric.Point(this.left, this.top), + + rotated = fabric.util.rotatePoint( + pClicked, pLeftTop, fabric.util.degreesToRadians(-this.angle)); + + return this.getLocalPointer(e, rotated); + }, + + /** + * 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._getLocalRotatedPointer(e), + textLines = this.text.split(this._reNewline), + prevWidth = 0, + width = 0, + height = 0, + charIndex = 0, + newSelectionStart; + + for (var i = 0, len = textLines.length; i < len; i++) { + + height += this._getHeightOfLine(this.ctx, i) * this.scaleY; + + var widthOfLine = this._getWidthOfLine(this.ctx, i, textLines); + var lineLeftOffset = this._getLineLeftOffset(widthOfLine); + + width = lineLeftOffset * this.scaleX; + + if (this.flipX) { + // when oject is horizontally flipped we reverse chars + textLines[i] = textLines[i].split('').reverse().join(''); + } + + for (var j = 0, jlen = textLines[i].length; j < jlen; j++) { + + var _char = textLines[i][j]; + prevWidth = width; + + width += this._getWidthOfChar(this.ctx, _char, 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) { + return this._getNewSelectionStartFromOffset( + mouseOffset, prevWidth, width, charIndex + i, jlen, j); + } + } + + // 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, j) { + + 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; + } + + if (j === jlen) { + newSelectionStart--; + } + + return newSelectionStart; + } +}); + + +fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + + /** + * Initializes key handlers + */ + initKeyHandlers: function() { + fabric.util.addListener(fabric.document, 'keydown', this.onKeyDown.bind(this)); + fabric.util.addListener(fabric.document, 'keypress', this.onKeyPress.bind(this)); + }, + + /** + * Initializes hidden textarea (needed to bring up keyboard in iOS) + */ + initHiddenTextarea: function() { + this.hiddenTextarea = fabric.document.createElement('textarea'); + + this.hiddenTextarea.setAttribute('autocapitalize', 'off'); + this.hiddenTextarea.style.cssText = 'position: absolute; top: 0; left: -9999px'; + + fabric.document.body.appendChild(this.hiddenTextarea); + }, + + /** + * @private + */ + _keysMap: { + 8: 'removeChars', + 13: 'insertNewline', + 37: 'moveCursorLeft', + 38: 'moveCursorUp', + 39: 'moveCursorRight', + 40: 'moveCursorDown', + 46: 'forwardDelete' + }, + + /** + * @private + */ + _ctrlKeysMap: { + 65: 'selectAll', + 67: 'copy', + 86: 'paste', + 88: 'cut' + }, + + /** + * 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._ctrlKeysMap) && (e.ctrlKey || e.metaKey)) { + this[this._ctrlKeysMap[e.keyCode]](e); + } + else { + return; + } + + e.preventDefault(); + e.stopPropagation(); + + this.canvas && this.canvas.renderAll(); + }, + + /** + * Forward delete + */ + forwardDelete: function(e) { + if (this.selectionStart === this.selectionEnd) { + this.moveCursorRight(e); + } + this.removeChars(e); + }, + + /** + * Copies selected text + */ + copy: function() { + var selectedText = this.getSelectedText(); + this.copiedText = selectedText; + this.copiedStyles = this.getSelectionStyles( + this.selectionStart, + this.selectionEnd); + }, + + /** + * Pastes text + */ + paste: function() { + if (this.copiedText) { + this.insertChars(this.copiedText); + } + }, + + /** + * Cuts text + */ + cut: function(e) { + this.copy(); + this.removeChars(e); + }, + + /** + * Handles keypress event + * @param {Event} e Event object + */ + onKeyPress: function(e) { + if (!this.isEditing || e.metaKey || e.ctrlKey || e.keyCode === 8 || e.keyCode === 13) { + return; + } + + this.insertChars(String.fromCharCode(e.which)); + + e.preventDefault(); + e.stopPropagation(); + }, + + /** + * Gets start offset of a selection + * @return {Number} + */ + getDownCursorOffset: function(e, isRight) { + + var selectionProp = isRight ? this.selectionEnd : this.selectionStart, + textLines = this.text.split(this._reNewline), + _char, + lineLeftOffset, + + textBeforeCursor = this.text.slice(0, selectionProp), + textAfterCursor = this.text.slice(selectionProp), + + textOnSameLineBeforeCursor = textBeforeCursor.slice(textBeforeCursor.lastIndexOf('\n') + 1), + textOnSameLineAfterCursor = textAfterCursor.match(/(.*)\n?/)[1], + textOnNextLine = (textAfterCursor.match(/.*\n(.*)\n?/) || { })[1] || '', + + cursorLocation = this.get2DCursorLocation(selectionProp); + + // if on last line, down cursor goes to end of line + if (cursorLocation.lineIndex === textLines.length - 1 || e.metaKey) { + + // move to the end of a text + return this.text.length - selectionProp; + } + + var widthOfSameLineBeforeCursor = this._getWidthOfLine(this.ctx, cursorLocation.lineIndex, textLines); + lineLeftOffset = this._getLineLeftOffset(widthOfSameLineBeforeCursor); + + var widthOfCharsOnSameLineBeforeCursor = lineLeftOffset; + var lineIndex = cursorLocation.lineIndex; + + for (var i = 0, len = textOnSameLineBeforeCursor.length; i < len; i++) { + _char = textOnSameLineBeforeCursor[i]; + widthOfCharsOnSameLineBeforeCursor += this._getWidthOfChar(this.ctx, _char, lineIndex, i); + } + + var indexOnNextLine = this._getIndexOnNextLine( + cursorLocation, textOnNextLine, widthOfCharsOnSameLineBeforeCursor, textLines); + + return textOnSameLineAfterCursor.length + 1 + indexOnNextLine; + }, + + /** + * @private + */ + _getIndexOnNextLine: function(cursorLocation, textOnNextLine, widthOfCharsOnSameLineBeforeCursor, textLines) { + + var lineIndex = cursorLocation.lineIndex + 1; + var widthOfNextLine = this._getWidthOfLine(this.ctx, lineIndex, textLines); + var lineLeftOffset = this._getLineLeftOffset(widthOfNextLine); + var widthOfCharsOnNextLine = lineLeftOffset; + var indexOnNextLine = 0; + var foundMatch; + + for (var j = 0, jlen = textOnNextLine.length; j < jlen; j++) { + + var _char = textOnNextLine[j]; + var widthOfChar = this._getWidthOfChar(this.ctx, _char, lineIndex, j); + + widthOfCharsOnNextLine += widthOfChar; + + if (widthOfCharsOnNextLine > widthOfCharsOnSameLineBeforeCursor) { + + foundMatch = true; + + var leftEdge = widthOfCharsOnNextLine - widthOfChar; + var rightEdge = widthOfCharsOnNextLine; + var offsetFromLeftEdge = Math.abs(leftEdge - widthOfCharsOnSameLineBeforeCursor); + var offsetFromRightEdge = Math.abs(rightEdge - widthOfCharsOnSameLineBeforeCursor); + + indexOnNextLine = offsetFromRightEdge < offsetFromLeftEdge ? j + 1 : j; + + break; + } + } + + // reached end + if (!foundMatch) { + indexOnNextLine = textOnNextLine.length; + } + + return indexOnNextLine; + }, + + /** + * Moves cursor down + * @param {Event} e Event object + */ + moveCursorDown: function(e) { + + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + + var offset = this.getDownCursorOffset(e, this._selectionDirection === 'right'); + + if (e.shiftKey) { + this.moveCursorDownWithShift(offset); + } + else { + this.moveCursorDownWithoutShift(offset); + } + + this.initDelayedCursor(); + }, + + /** + * Moves cursor down without keeping selection + * @param {Number} offset + */ + moveCursorDownWithoutShift: function(offset) { + + this._selectionDirection = 'right'; + this.selectionStart += offset; + + if (this.selectionStart > this.text.length) { + this.selectionStart = this.text.length; + } + this.selectionEnd = this.selectionStart; + }, + + /** + * Moves cursor down while keeping selection + * @param {Number} offset + */ + moveCursorDownWithShift: function(offset) { + + if (this._selectionDirection === 'left' && (this.selectionStart !== this.selectionEnd)) { + this.selectionStart += offset; + this._selectionDirection = 'left'; + return; + } + else { + this._selectionDirection = 'right'; + this.selectionEnd += offset; + + if (this.selectionEnd > this.text.length) { + this.selectionEnd = this.text.length; + } + } + }, + + getUpCursorOffset: function(e, isRight) { + + var selectionProp = isRight ? this.selectionEnd : this.selectionStart, + cursorLocation = this.get2DCursorLocation(selectionProp); + + // if on first line, up cursor goes to start of line + if (cursorLocation.lineIndex === 0 || e.metaKey) { + return selectionProp; + } + + var textBeforeCursor = this.text.slice(0, selectionProp), + textOnSameLineBeforeCursor = textBeforeCursor.slice(textBeforeCursor.lastIndexOf('\n') + 1), + textOnPreviousLine = (textBeforeCursor.match(/\n?(.*)\n.*$/) || {})[1] || '', + textLines = this.text.split(this._reNewline), + _char, + lineLeftOffset; + + var widthOfSameLineBeforeCursor = this._getWidthOfLine(this.ctx, cursorLocation.lineIndex, textLines); + lineLeftOffset = this._getLineLeftOffset(widthOfSameLineBeforeCursor); + + var widthOfCharsOnSameLineBeforeCursor = lineLeftOffset; + var lineIndex = cursorLocation.lineIndex; + + for (var i = 0, len = textOnSameLineBeforeCursor.length; i < len; i++) { + _char = textOnSameLineBeforeCursor[i]; + widthOfCharsOnSameLineBeforeCursor += this._getWidthOfChar(this.ctx, _char, lineIndex, i); + } + + var indexOnPrevLine = this._getIndexOnPrevLine( + cursorLocation, textOnPreviousLine, widthOfCharsOnSameLineBeforeCursor, textLines); + + return textOnPreviousLine.length - indexOnPrevLine + textOnSameLineBeforeCursor.length; + }, + + /** + * @private + */ + _getIndexOnPrevLine: function(cursorLocation, textOnPreviousLine, widthOfCharsOnSameLineBeforeCursor, textLines) { + + var lineIndex = cursorLocation.lineIndex - 1; + var widthOfPreviousLine = this._getWidthOfLine(this.ctx, lineIndex, textLines); + var lineLeftOffset = this._getLineLeftOffset(widthOfPreviousLine); + var widthOfCharsOnPreviousLine = lineLeftOffset; + var indexOnPrevLine = 0; + var foundMatch; + + for (var j = 0, jlen = textOnPreviousLine.length; j < jlen; j++) { + + var _char = textOnPreviousLine[j]; + var widthOfChar = this._getWidthOfChar(this.ctx, _char, lineIndex, j); + + widthOfCharsOnPreviousLine += widthOfChar; + + if (widthOfCharsOnPreviousLine > widthOfCharsOnSameLineBeforeCursor) { + + foundMatch = true; + + var leftEdge = widthOfCharsOnPreviousLine - widthOfChar; + var rightEdge = widthOfCharsOnPreviousLine; + var offsetFromLeftEdge = Math.abs(leftEdge - widthOfCharsOnSameLineBeforeCursor); + var offsetFromRightEdge = Math.abs(rightEdge - widthOfCharsOnSameLineBeforeCursor); + + indexOnPrevLine = offsetFromRightEdge < offsetFromLeftEdge ? j : (j - 1); + + break; + } + } + + // reached end + if (!foundMatch) { + indexOnPrevLine = textOnPreviousLine.length - 1; + } + + return indexOnPrevLine; + }, + + /** + * Moves cursor up + * @param {Event} e Event object + */ + moveCursorUp: function(e) { + + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + + var offset = this.getUpCursorOffset(e, this._selectionDirection === 'right'); + + if (e.shiftKey) { + this.moveCursorUpWithShift(offset); + } + else { + this.moveCursorUpWithoutShift(offset); + } + + this.initDelayedCursor(); + }, + + /** + * Moves cursor up with shift + * @param {Number} offset + */ + moveCursorUpWithShift: function(offset) { + + if (this.selectionStart === this.selectionEnd) { + this.selectionStart -= offset; + } + else { + if (this._selectionDirection === 'right') { + this.selectionEnd -= offset; + this._selectionDirection = 'right'; + return; + } + else { + this.selectionStart -= offset; + } + } + + if (this.selectionStart < 0) { + this.selectionStart = 0; + } + + this._selectionDirection = 'left'; + }, + + /** + * Moves cursor up without shift + * @param {Number} offset + */ + moveCursorUpWithoutShift: function(offset) { + if (this.selectionStart === this.selectionEnd) { + this.selectionStart -= offset; + } + if (this.selectionStart < 0) { + this.selectionStart = 0; + } + this.selectionEnd = this.selectionStart; + + this._selectionDirection = 'left'; + }, + + /** + * Moves cursor left + * @param {Event} e Event object + */ + moveCursorLeft: function(e) { + if (this.selectionStart === 0 && this.selectionEnd === 0) return; + + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + + if (e.shiftKey) { + this.moveCursorLeftWithShift(e); + } + else { + this.moveCursorLeftWithoutShift(e); + } + + this.initDelayedCursor(); + }, + + /** + * @private + */ + _move: function(e, prop, direction) { + if (e.altKey) { + this[prop] = this['findWordBoundary' + direction](this[prop]); + } + else if (e.metaKey) { + this[prop] = this['findLineBoundary' + direction](this[prop]); + } + else { + this[prop] += (direction === 'Left' ? -1 : 1); + } + }, + + /** + * @private + */ + _moveLeft: function(e, prop) { + this._move(e, prop, 'Left'); + }, + + /** + * @private + */ + _moveRight: function(e, prop) { + this._move(e, prop, 'Right'); + }, + + /** + * Moves cursor left without keeping selection + * @param {Event} e + */ + moveCursorLeftWithoutShift: function(e) { + 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._moveLeft(e, 'selectionStart'); + } + this.selectionEnd = this.selectionStart; + }, + + /** + * Moves cursor left while keeping selection + * @param {Event} e + */ + moveCursorLeftWithShift: function(e) { + if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) { + this._moveLeft(e, 'selectionEnd'); + } + else { + this._selectionDirection = 'left'; + this._moveLeft(e, 'selectionStart'); + + // increase selection by one if it's a newline + if (this.text.charAt(this.selectionStart) === '\n') { + this.selectionStart--; + } + if (this.selectionStart < 0) { + this.selectionStart = 0; + } + } + }, + + /** + * Moves cursor right + * @param {Event} e Event object + */ + moveCursorRight: function(e) { + if (this.selectionStart >= this.text.length && this.selectionEnd >= this.text.length) return; + + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + + if (e.shiftKey) { + this.moveCursorRightWithShift(e); + } + else { + this.moveCursorRightWithoutShift(e); + } + + this.initDelayedCursor(); + }, + + /** + * Moves cursor right while keeping selection + * @param {Event} e + */ + moveCursorRightWithShift: function(e) { + if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { + this._moveRight(e, 'selectionStart'); + } + else { + this._selectionDirection = 'right'; + this._moveRight(e, 'selectionEnd'); + + // increase selection by one if it's a newline + if (this.text.charAt(this.selectionEnd - 1) === '\n') { + this.selectionEnd++; + } + if (this.selectionEnd > this.text.length) { + this.selectionEnd = this.text.length; + } + } + }, + + /** + * Moves cursor right without keeping selection + * @param {Event} e + */ + moveCursorRightWithoutShift: function(e) { + this._selectionDirection = 'right'; + + if (this.selectionStart === this.selectionEnd) { + this._moveRight(e, 'selectionStart'); + this.selectionEnd = this.selectionStart; + } + else { + this.selectionEnd += this.getNumNewLinesInSelectedText(); + if (this.selectionEnd > this.text.length) { + this.selectionEnd = this.text.length; + } + this.selectionStart = this.selectionEnd; + } + }, + + /** + * Inserts a character where cursor is (replacing selection if one exists) + */ + removeChars: function(e) { + if (this.selectionStart === this.selectionEnd) { + this._removeCharsNearCursor(e); + } + else { + this._removeCharsFromTo(this.selectionStart, this.selectionEnd); + } + + this.selectionEnd = this.selectionStart; + + this._removeExtraneousStyles(); + + if (this.canvas) { + // TODO: double renderAll gets rid of text box shift happenning sometimes + // need to find out what exactly causes it and fix it + this.canvas.renderAll().renderAll(); + } + + this.setCoords(); + this.fire('changed'); + this.canvas && this.canvas.fire('text:changed', { target: this }); + }, + + /** + * @private + */ + _removeCharsNearCursor: function(e) { + if (this.selectionStart !== 0) { + + if (e.metaKey) { + // remove all till the start of current line + var leftLineBoundary = this.findLineBoundaryLeft(this.selectionStart); + + this._removeCharsFromTo(leftLineBoundary, this.selectionStart); + this.selectionStart = 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.selectionStart = leftWordBoundary; + } + else { + var isBeginningOfLine = this.text.slice(this.selectionStart-1, this.selectionStart) === '\n'; + this.removeStyleObject(isBeginningOfLine); + + this.selectionStart--; + this.text = this.text.slice(0, this.selectionStart) + + this.text.slice(this.selectionStart + 1); + } + } + } +}); + + +/* _TO_SVG_START_ */ +fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + + /** + * @private + */ + _setSVGTextLineText: function(textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects) { + if (!this.styles[lineIndex]) { + this.callSuper('_setSVGTextLineText', + textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier); + } + else { + this._setSVGTextLineChars( + textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects); + } + }, + + /** + * @private + */ + _setSVGTextLineChars: function(textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects) { + + var yProp = lineIndex === 0 || this.useNative ? 'y' : 'dy', + chars = textLine.split(''), + charOffset = 0, + lineLeftOffset = this._getSVGLineLeftOffset(lineIndex), + lineTopOffset = this._getSVGLineTopOffset(lineIndex), + heightOfLine = this._getHeightOfLine(this.ctx, lineIndex); + + for (var i = 0, len = chars.length; i < len; i++) { + var styleDecl = this.styles[lineIndex][i] || { }; + + textSpans.push( + this._createTextCharSpan( + chars[i], styleDecl, lineLeftOffset, lineTopOffset, yProp, charOffset)); + + var charWidth = this._getWidthOfChar(this.ctx, chars[i], lineIndex, i); + + if (styleDecl.textBackgroundColor) { + textBgRects.push( + this._createTextCharBg( + styleDecl, lineLeftOffset, lineTopOffset, heightOfLine, charWidth, charOffset)); + } + + charOffset += charWidth; + } + }, + + /** + * @private + */ + _getSVGLineLeftOffset: function(lineIndex) { + return (this._boundaries && this._boundaries[lineIndex]) + ? fabric.util.toFixed(this._boundaries[lineIndex].left, 2) + : 0; + }, + + /** + * @private + */ + _getSVGLineTopOffset: function(lineIndex) { + var lineTopOffset = 0; + for (var j = 0; j <= lineIndex; j++) { + lineTopOffset += this._getHeightOfLine(this.ctx, j); + } + return lineTopOffset - this.height / 2; + }, + + /** + * @private + */ + _createTextCharBg: function(styleDecl, lineLeftOffset, lineTopOffset, heightOfLine, charWidth, charOffset) { + return [ + '' + ].join(''); + }, + + /** + * @private + */ + _createTextCharSpan: function(_char, styleDecl, lineLeftOffset, lineTopOffset, yProp, charOffset) { + + var fillStyles = this.getSvgStyles.call(fabric.util.object.extend({ + visible: true, + fill: this.fill, + stroke: this.stroke, + type: 'text' + }, styleDecl)); + + return [ + '', + + fabric.util.string.escapeXml(_char), + '' + ].join(''); + } +}); +/* _TO_SVG_END_ */ + + +(function() { + + if (typeof document !== 'undefined' && typeof window !== 'undefined') { + return; + } + + var DOMParser = new 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.port === 443 ) ? HTTPS : HTTP; + + var 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); + } + }); + + req.end(); + } + + /** @private */ + function request_fs(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) { + var createImageAndCallBack = function(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); + }; + 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) { + request_fs(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) { + request_fs(url, function(body) { + fabric.loadSVGFromString(body, 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) { + eval(body); + callback && callback(); + }); + }; + + fabric.Image.fromObject = function(object, callback) { + fabric.util.loadImage(object.src, function(img) { + var oImg = new fabric.Image(img); + + oImg._initConfig(object); + oImg._initFilters(object, function(filters) { + oImg.filters = filters || [ ]; + callback && callback(oImg); + }); + }); + }; + + /** + * Only available when running fabric on node.js + * @param width Canvas width + * @param height Canvas height + * @param {Object} options to pass to FabricCanvas. + * @return {Object} wrapped canvas instance + */ + fabric.createCanvasForNode = function(width, height, options) { + + var canvasEl = fabric.document.createElement('canvas'), + nodeCanvas = new Canvas(width || 600, height || 600); + + // jsdom doesn't create style on canvas element, so here be temp. workaround + canvasEl.style = { }; + + canvasEl.width = nodeCanvas.width; + canvasEl.height = nodeCanvas.height; + + var FabricCanvas = fabric.Canvas || fabric.StaticCanvas; + var fabricCanvas = new FabricCanvas(canvasEl, options); + fabricCanvas.contextContainer = nodeCanvas.getContext('2d'); + fabricCanvas.nodeCanvas = nodeCanvas; + fabricCanvas.Font = Canvas.Font; + + return fabricCanvas; + }; + + /** @ignore */ + fabric.StaticCanvas.prototype.createPNGStream = function() { + return this.nodeCanvas.createPNGStream(); + }; + + fabric.StaticCanvas.prototype.createJPEGStream = function(opts) { + return this.nodeCanvas.createJPEGStream(opts); + }; + + var origSetWidth = fabric.StaticCanvas.prototype.setWidth; + fabric.StaticCanvas.prototype.setWidth = function(width) { + origSetWidth.call(this, width); + this.nodeCanvas.width = width; + return this; + }; + if (fabric.Canvas) { + fabric.Canvas.prototype.setWidth = fabric.StaticCanvas.prototype.setWidth; + } + + var origSetHeight = fabric.StaticCanvas.prototype.setHeight; + fabric.StaticCanvas.prototype.setHeight = function(height) { + origSetHeight.call(this, height); + this.nodeCanvas.height = height; + return this; + }; + if (fabric.Canvas) { + fabric.Canvas.prototype.setHeight = fabric.StaticCanvas.prototype.setHeight; + } + +})(); + diff --git a/lib/pixi.dev.js b/lib/pixi.dev.js new file mode 100644 index 0000000..388c9d9 --- /dev/null +++ b/lib/pixi.dev.js @@ -0,0 +1,14643 @@ +/** + * @license + * pixi.js - v1.5.3 + * Copyright (c) 2012-2014, Mat Groves + * http://goodboydigital.com/ + * + * Compiled: 2014-04-24 + * + * pixi.js is licensed under the MIT License. + * http://www.opensource.org/licenses/mit-license.php + */ +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +(function(){ + + var root = this; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +/** + * @module PIXI + */ +var PIXI = PIXI || {}; + +/* +* +* This file contains a lot of pixi consts which are used across the rendering engine +* @class Consts +*/ +PIXI.WEBGL_RENDERER = 0; +PIXI.CANVAS_RENDERER = 1; + +// useful for testing against if your lib is using pixi. +PIXI.VERSION = "v1.5.3"; + +// the various blend modes supported by pixi +PIXI.blendModes = { + NORMAL:0, + ADD:1, + MULTIPLY:2, + SCREEN:3, + OVERLAY:4, + DARKEN:5, + LIGHTEN:6, + COLOR_DODGE:7, + COLOR_BURN:8, + HARD_LIGHT:9, + SOFT_LIGHT:10, + DIFFERENCE:11, + EXCLUSION:12, + HUE:13, + SATURATION:14, + COLOR:15, + LUMINOSITY:16 +}; + +// the scale modes +PIXI.scaleModes = { + DEFAULT:0, + LINEAR:0, + NEAREST:1 +}; + +// interaction frequency +PIXI.INTERACTION_FREQUENCY = 30; +PIXI.AUTO_PREVENT_DEFAULT = true; + +PIXI.RAD_TO_DEG = 180 / Math.PI; +PIXI.DEG_TO_RAD = Math.PI / 180; +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +/** + * The Point object represents a location in a two-dimensional coordinate system, where x represents the horizontal axis and y represents the vertical axis. + * + * @class Point + * @constructor + * @param x {Number} position of the point on the x axis + * @param y {Number} position of the point on the y axis + */ +PIXI.Point = function(x, y) +{ + /** + * @property x + * @type Number + * @default 0 + */ + this.x = x || 0; + + /** + * @property y + * @type Number + * @default 0 + */ + this.y = y || 0; +}; + +/** + * Creates a clone of this point + * + * @method clone + * @return {Point} a copy of the point + */ +PIXI.Point.prototype.clone = function() +{ + return new PIXI.Point(this.x, this.y); +}; + +// constructor +PIXI.Point.prototype.constructor = PIXI.Point; + +PIXI.Point.prototype.set = function(x, y) +{ + this.x = x || 0; + this.y = y || ( (y !== 0) ? this.x : 0 ) ; +}; + + +/** + * @author Mat Groves http://matgroves.com/ + */ + +/** + * the Rectangle object is an area defined by its position, as indicated by its top-left corner point (x, y) and by its width and its height. + * + * @class Rectangle + * @constructor + * @param x {Number} The X coord of the upper-left corner of the rectangle + * @param y {Number} The Y coord of the upper-left corner of the rectangle + * @param width {Number} The overall width of this rectangle + * @param height {Number} The overall height of this rectangle + */ +PIXI.Rectangle = function(x, y, width, height) +{ + /** + * @property x + * @type Number + * @default 0 + */ + this.x = x || 0; + + /** + * @property y + * @type Number + * @default 0 + */ + this.y = y || 0; + + /** + * @property width + * @type Number + * @default 0 + */ + this.width = width || 0; + + /** + * @property height + * @type Number + * @default 0 + */ + this.height = height || 0; +}; + +/** + * Creates a clone of this Rectangle + * + * @method clone + * @return {Rectangle} a copy of the rectangle + */ +PIXI.Rectangle.prototype.clone = function() +{ + return new PIXI.Rectangle(this.x, this.y, this.width, this.height); +}; + +/** + * Checks whether the x and y coordinates passed to this function are contained within this Rectangle + * + * @method contains + * @param x {Number} The X coordinate of the point to test + * @param y {Number} The Y coordinate of the point to test + * @return {Boolean} Whether the x/y coords are within this Rectangle + */ +PIXI.Rectangle.prototype.contains = function(x, y) +{ + if(this.width <= 0 || this.height <= 0) + return false; + + var x1 = this.x; + if(x >= x1 && x <= x1 + this.width) + { + var y1 = this.y; + + if(y >= y1 && y <= y1 + this.height) + { + return true; + } + } + + return false; +}; + +// constructor +PIXI.Rectangle.prototype.constructor = PIXI.Rectangle; + +PIXI.EmptyRectangle = new PIXI.Rectangle(0,0,0,0); +/** + * @author Adrien Brault + */ + +/** + * @class Polygon + * @constructor + * @param points* {Array|Array|Point...|Number...} This can be an array of Points that form the polygon, + * a flat array of numbers that will be interpreted as [x,y, x,y, ...], or the arguments passed can be + * all the points of the polygon e.g. `new PIXI.Polygon(new PIXI.Point(), new PIXI.Point(), ...)`, or the + * arguments passed can be flat x,y values e.g. `new PIXI.Polygon(x,y, x,y, x,y, ...)` where `x` and `y` are + * Numbers. + */ +PIXI.Polygon = function(points) +{ + //if points isn't an array, use arguments as the array + if(!(points instanceof Array)) + points = Array.prototype.slice.call(arguments); + + //if this is a flat array of numbers, convert it to points + if(typeof points[0] === 'number') { + var p = []; + for(var i = 0, il = points.length; i < il; i+=2) { + p.push( + new PIXI.Point(points[i], points[i + 1]) + ); + } + + points = p; + } + + this.points = points; +}; + +/** + * Creates a clone of this polygon + * + * @method clone + * @return {Polygon} a copy of the polygon + */ +PIXI.Polygon.prototype.clone = function() +{ + var points = []; + for (var i=0; i y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi); + + if(intersect) inside = !inside; + } + + return inside; +}; + +// constructor +PIXI.Polygon.prototype.constructor = PIXI.Polygon; + +/** + * @author Chad Engler + */ + +/** + * The Circle object can be used to specify a hit area for displayObjects + * + * @class Circle + * @constructor + * @param x {Number} The X coordinate of the center of this circle + * @param y {Number} The Y coordinate of the center of this circle + * @param radius {Number} The radius of the circle + */ +PIXI.Circle = function(x, y, radius) +{ + /** + * @property x + * @type Number + * @default 0 + */ + this.x = x || 0; + + /** + * @property y + * @type Number + * @default 0 + */ + this.y = y || 0; + + /** + * @property radius + * @type Number + * @default 0 + */ + this.radius = radius || 0; +}; + +/** + * Creates a clone of this Circle instance + * + * @method clone + * @return {Circle} a copy of the polygon + */ +PIXI.Circle.prototype.clone = function() +{ + return new PIXI.Circle(this.x, this.y, this.radius); +}; + +/** + * Checks whether the x, and y coordinates passed to this function are contained within this circle + * + * @method contains + * @param x {Number} The X coordinate of the point to test + * @param y {Number} The Y coordinate of the point to test + * @return {Boolean} Whether the x/y coordinates are within this polygon + */ +PIXI.Circle.prototype.contains = function(x, y) +{ + if(this.radius <= 0) + return false; + + var dx = (this.x - x), + dy = (this.y - y), + r2 = this.radius * this.radius; + + dx *= dx; + dy *= dy; + + return (dx + dy <= r2); +}; + +// constructor +PIXI.Circle.prototype.constructor = PIXI.Circle; + + +/** + * @author Chad Engler + */ + +/** + * The Ellipse object can be used to specify a hit area for displayObjects + * + * @class Ellipse + * @constructor + * @param x {Number} The X coordinate of the upper-left corner of the framing rectangle of this ellipse + * @param y {Number} The Y coordinate of the upper-left corner of the framing rectangle of this ellipse + * @param width {Number} The overall width of this ellipse + * @param height {Number} The overall height of this ellipse + */ +PIXI.Ellipse = function(x, y, width, height) +{ + /** + * @property x + * @type Number + * @default 0 + */ + this.x = x || 0; + + /** + * @property y + * @type Number + * @default 0 + */ + this.y = y || 0; + + /** + * @property width + * @type Number + * @default 0 + */ + this.width = width || 0; + + /** + * @property height + * @type Number + * @default 0 + */ + this.height = height || 0; +}; + +/** + * Creates a clone of this Ellipse instance + * + * @method clone + * @return {Ellipse} a copy of the ellipse + */ +PIXI.Ellipse.prototype.clone = function() +{ + return new PIXI.Ellipse(this.x, this.y, this.width, this.height); +}; + +/** + * Checks whether the x and y coordinates passed to this function are contained within this ellipse + * + * @method contains + * @param x {Number} The X coordinate of the point to test + * @param y {Number} The Y coordinate of the point to test + * @return {Boolean} Whether the x/y coords are within this ellipse + */ +PIXI.Ellipse.prototype.contains = function(x, y) +{ + if(this.width <= 0 || this.height <= 0) + return false; + + //normalize the coords to an ellipse with center 0,0 + var normx = ((x - this.x) / this.width), + normy = ((y - this.y) / this.height); + + normx *= normx; + normy *= normy; + + return (normx + normy <= 1); +}; + +/** +* Returns the framing rectangle of the ellipse as a PIXI.Rectangle object +* +* @method getBounds +* @return {Rectangle} the framing rectangle +*/ +PIXI.Ellipse.prototype.getBounds = function() +{ + return new PIXI.Rectangle(this.x, this.y, this.width, this.height); +}; + +// constructor +PIXI.Ellipse.prototype.constructor = PIXI.Ellipse; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +PIXI.determineMatrixArrayType = function() { + return (typeof Float32Array !== 'undefined') ? Float32Array : Array; +}; + +/* +* @class Matrix2 +* The Matrix2 class will choose the best type of array to use between +* a regular javascript Array and a Float32Array if the latter is available +* +*/ +PIXI.Matrix2 = PIXI.determineMatrixArrayType(); + +/* +* @class Matrix +* The Matrix class is now an object, which makes it a lot faster, +* here is a representation of it : +* | a | b | tx| +* | c | c | ty| +* | 0 | 0 | 1 | +* +*/ +PIXI.Matrix = function() +{ + this.a = 1; + this.b = 0; + this.c = 0; + this.d = 1; + this.tx = 0; + this.ty = 0; +}; + +/** + * Creates a pixi matrix object based on the array given as a parameter + * + * @method fromArray + * @param array {Array} The array that the matrix will be filled with + */ +PIXI.Matrix.prototype.fromArray = function(array) +{ + this.a = array[0]; + this.b = array[1]; + this.c = array[3]; + this.d = array[4]; + this.tx = array[2]; + this.ty = array[5]; +}; + +/** + * Creates an array from the current Matrix object + * + * @method toArray + * @param transpose {Boolean} Whether we need to transpose the matrix or not + * @return array {Array} the newly created array which contains the matrix + */ +PIXI.Matrix.prototype.toArray = function(transpose) +{ + if(!this.array) this.array = new Float32Array(9); + var array = this.array; + + if(transpose) + { + this.array[0] = this.a; + this.array[1] = this.c; + this.array[2] = 0; + this.array[3] = this.b; + this.array[4] = this.d; + this.array[5] = 0; + this.array[6] = this.tx; + this.array[7] = this.ty; + this.array[8] = 1; + } + else + { + this.array[0] = this.a; + this.array[1] = this.b; + this.array[2] = this.tx; + this.array[3] = this.c; + this.array[4] = this.d; + this.array[5] = this.ty; + this.array[6] = 0; + this.array[7] = 0; + this.array[8] = 1; + } + + return array;//[this.a, this.b, this.tx, this.c, this.d, this.ty, 0, 0, 1]; +}; + +PIXI.identityMatrix = new PIXI.Matrix(); +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +/** + * The base class for all objects that are rendered on the screen. + * This is an abstract class and should not be used on its own rather it should be extended. + * + * @class DisplayObject + * @constructor + */ +PIXI.DisplayObject = function() +{ + /** + * The coordinate of the object relative to the local coordinates of the parent. + * + * @property position + * @type Point + */ + this.position = new PIXI.Point(); + + /** + * The scale factor of the object. + * + * @property scale + * @type Point + */ + this.scale = new PIXI.Point(1,1);//{x:1, y:1}; + + /** + * The pivot point of the displayObject that it rotates around + * + * @property pivot + * @type Point + */ + this.pivot = new PIXI.Point(0,0); + + /** + * The rotation of the object in radians. + * + * @property rotation + * @type Number + */ + this.rotation = 0; + + /** + * The opacity of the object. + * + * @property alpha + * @type Number + */ + this.alpha = 1; + + /** + * The visibility of the object. + * + * @property visible + * @type Boolean + */ + this.visible = true; + + /** + * This is the defined area that will pick up mouse / touch events. It is null by default. + * Setting it is a neat way of optimising the hitTest function that the interactionManager will use (as it will not need to hit test all the children) + * + * @property hitArea + * @type Rectangle|Circle|Ellipse|Polygon + */ + this.hitArea = null; + + /** + * This is used to indicate if the displayObject should display a mouse hand cursor on rollover + * + * @property buttonMode + * @type Boolean + */ + this.buttonMode = false; + + /** + * Can this object be rendered + * + * @property renderable + * @type Boolean + */ + this.renderable = false; + + /** + * [read-only] The display object container that contains this display object. + * + * @property parent + * @type DisplayObjectContainer + * @readOnly + */ + this.parent = null; + + /** + * [read-only] The stage the display object is connected to, or undefined if it is not connected to the stage. + * + * @property stage + * @type Stage + * @readOnly + */ + this.stage = null; + + /** + * [read-only] The multiplied alpha of the displayObject + * + * @property worldAlpha + * @type Number + * @readOnly + */ + this.worldAlpha = 1; + + /** + * [read-only] Whether or not the object is interactive, do not toggle directly! use the `interactive` property + * + * @property _interactive + * @type Boolean + * @readOnly + * @private + */ + this._interactive = false; + + /** + * This is the cursor that will be used when the mouse is over this object. To enable this the element must have interaction = true and buttonMode = true + * + * @property defaultCursor + * @type String + * + */ + this.defaultCursor = 'pointer'; + + /** + * [read-only] Current transform of the object based on world (parent) factors + * + * @property worldTransform + * @type Mat3 + * @readOnly + * @private + */ + this.worldTransform = new PIXI.Matrix(); + + /** + * [NYI] Unknown + * + * @property color + * @type Array<> + * @private + */ + this.color = []; + + /** + * [NYI] Holds whether or not this object is dynamic, for rendering optimization + * + * @property dynamic + * @type Boolean + * @private + */ + this.dynamic = true; + + // cached sin rotation and cos rotation + this._sr = 0; + this._cr = 1; + + /** + * The area the filter is applied to like the hitArea this is used as more of an optimisation + * rather than figuring out the dimensions of the displayObject each frame you can set this rectangle + * + * @property filterArea + * @type Rectangle + */ + this.filterArea = null;//new PIXI.Rectangle(0,0,1,1); + + /** + * The original, cached bounds of the object + * + * @property _bounds + * @type Rectangle + * @private + */ + this._bounds = new PIXI.Rectangle(0, 0, 1, 1); + /** + * The most up-to-date bounds of the object + * + * @property _currentBounds + * @type Rectangle + * @private + */ + this._currentBounds = null; + /** + * The original, cached mask of the object + * + * @property _currentBounds + * @type Rectangle + * @private + */ + this._mask = null; + + this._cacheAsBitmap = false; + this._cacheIsDirty = false; + + + /* + * MOUSE Callbacks + */ + + /** + * A callback that is used when the users clicks on the displayObject with their mouse + * @method click + * @param interactionData {InteractionData} + */ + + /** + * A callback that is used when the user clicks the mouse down over the sprite + * @method mousedown + * @param interactionData {InteractionData} + */ + + /** + * A callback that is used when the user releases the mouse that was over the displayObject + * for this callback to be fired the mouse must have been pressed down over the displayObject + * @method mouseup + * @param interactionData {InteractionData} + */ + + /** + * A callback that is used when the user releases the mouse that was over the displayObject but is no longer over the displayObject + * for this callback to be fired, The touch must have started over the displayObject + * @method mouseupoutside + * @param interactionData {InteractionData} + */ + + /** + * A callback that is used when the users mouse rolls over the displayObject + * @method mouseover + * @param interactionData {InteractionData} + */ + + /** + * A callback that is used when the users mouse leaves the displayObject + * @method mouseout + * @param interactionData {InteractionData} + */ + + + /* + * TOUCH Callbacks + */ + + /** + * A callback that is used when the users taps on the sprite with their finger + * basically a touch version of click + * @method tap + * @param interactionData {InteractionData} + */ + + /** + * A callback that is used when the user touches over the displayObject + * @method touchstart + * @param interactionData {InteractionData} + */ + + /** + * A callback that is used when the user releases a touch over the displayObject + * @method touchend + * @param interactionData {InteractionData} + */ + + /** + * A callback that is used when the user releases the touch that was over the displayObject + * for this callback to be fired, The touch must have started over the sprite + * @method touchendoutside + * @param interactionData {InteractionData} + */ +}; + +// constructor +PIXI.DisplayObject.prototype.constructor = PIXI.DisplayObject; + +/** + * [Deprecated] Indicates if the sprite will have touch and mouse interactivity. It is false by default + * Instead of using this function you can now simply set the interactive property to true or false + * + * @method setInteractive + * @param interactive {Boolean} + * @deprecated Simply set the `interactive` property directly + */ +PIXI.DisplayObject.prototype.setInteractive = function(interactive) +{ + this.interactive = interactive; +}; + +/** + * Indicates if the sprite will have touch and mouse interactivity. It is false by default + * + * @property interactive + * @type Boolean + * @default false + */ +Object.defineProperty(PIXI.DisplayObject.prototype, 'interactive', { + get: function() { + return this._interactive; + }, + set: function(value) { + this._interactive = value; + + // TODO more to be done here.. + // need to sort out a re-crawl! + if(this.stage)this.stage.dirty = true; + } +}); + +/** + * [read-only] Indicates if the sprite is globaly visible. + * + * @property worldVisible + * @type Boolean + */ +Object.defineProperty(PIXI.DisplayObject.prototype, 'worldVisible', { + get: function() { + var item = this; + + do + { + if(!item.visible)return false; + item = item.parent; + } + while(item); + + return true; + } +}); + +/** + * Sets a mask for the displayObject. A mask is an object that limits the visibility of an object to the shape of the mask applied to it. + * In PIXI a regular mask must be a PIXI.Graphics object. This allows for much faster masking in canvas as it utilises shape clipping. + * To remove a mask, set this property to null. + * + * @property mask + * @type Graphics + */ +Object.defineProperty(PIXI.DisplayObject.prototype, 'mask', { + get: function() { + return this._mask; + }, + set: function(value) { + + if(this._mask)this._mask.isMask = false; + this._mask = value; + if(this._mask)this._mask.isMask = true; + } +}); + +/** + * Sets the filters for the displayObject. + * * IMPORTANT: This is a webGL only feature and will be ignored by the canvas renderer. + * To remove filters simply set this property to 'null' + * @property filters + * @type Array An array of filters + */ +Object.defineProperty(PIXI.DisplayObject.prototype, 'filters', { + get: function() { + return this._filters; + }, + set: function(value) { + + if(value) + { + // now put all the passes in one place.. + var passes = []; + for (var i = 0; i < value.length; i++) + { + var filterPasses = value[i].passes; + for (var j = 0; j < filterPasses.length; j++) + { + passes.push(filterPasses[j]); + } + } + + // TODO change this as it is legacy + this._filterBlock = {target:this, filterPasses:passes}; + } + + this._filters = value; + } +}); + +/** + * Set weather or not a the display objects is cached as a bitmap. + * This basically takes a snap shot of the display object as it is at that moment. It can provide a performance benefit for complex static displayObjects + * To remove filters simply set this property to 'null' + * @property cacheAsBitmap + * @type Boolean + */ +Object.defineProperty(PIXI.DisplayObject.prototype, 'cacheAsBitmap', { + get: function() { + return this._cacheAsBitmap; + }, + set: function(value) { + + if(this._cacheAsBitmap === value)return; + + if(value) + { + //this._cacheIsDirty = true; + this._generateCachedSprite(); + } + else + { + this._destroyCachedSprite(); + } + + this._cacheAsBitmap = value; + } +}); + +/* + * Updates the object transform for rendering + * + * @method updateTransform + * @private + */ +PIXI.DisplayObject.prototype.updateTransform = function() +{ + // TODO OPTIMIZE THIS!! with dirty + if(this.rotation !== this.rotationCache) + { + + this.rotationCache = this.rotation; + this._sr = Math.sin(this.rotation); + this._cr = Math.cos(this.rotation); + } + + // var localTransform = this.localTransform//.toArray(); + var parentTransform = this.parent.worldTransform;//.toArray(); + var worldTransform = this.worldTransform;//.toArray(); + + var px = this.pivot.x; + var py = this.pivot.y; + + var a00 = this._cr * this.scale.x, + a01 = -this._sr * this.scale.y, + a10 = this._sr * this.scale.x, + a11 = this._cr * this.scale.y, + a02 = this.position.x - a00 * px - py * a01, + a12 = this.position.y - a11 * py - px * a10, + b00 = parentTransform.a, b01 = parentTransform.b, + b10 = parentTransform.c, b11 = parentTransform.d; + + worldTransform.a = b00 * a00 + b01 * a10; + worldTransform.b = b00 * a01 + b01 * a11; + worldTransform.tx = b00 * a02 + b01 * a12 + parentTransform.tx; + + worldTransform.c = b10 * a00 + b11 * a10; + worldTransform.d = b10 * a01 + b11 * a11; + worldTransform.ty = b10 * a02 + b11 * a12 + parentTransform.ty; + + this.worldAlpha = this.alpha * this.parent.worldAlpha; +}; + +/** + * Retrieves the bounds of the displayObject as a rectangle object + * + * @method getBounds + * @return {Rectangle} the rectangular bounding area + */ +PIXI.DisplayObject.prototype.getBounds = function( matrix ) +{ + matrix = matrix;//just to get passed js hinting (and preserve inheritance) + return PIXI.EmptyRectangle; +}; + +/** + * Retrieves the local bounds of the displayObject as a rectangle object + * + * @method getLocalBounds + * @return {Rectangle} the rectangular bounding area + */ +PIXI.DisplayObject.prototype.getLocalBounds = function() +{ + return this.getBounds(PIXI.identityMatrix);///PIXI.EmptyRectangle(); +}; + + +/** + * Sets the object's stage reference, the stage this object is connected to + * + * @method setStageReference + * @param stage {Stage} the stage that the object will have as its current stage reference + */ +PIXI.DisplayObject.prototype.setStageReference = function(stage) +{ + this.stage = stage; + if(this._interactive)this.stage.dirty = true; +}; + +PIXI.DisplayObject.prototype.generateTexture = function(renderer) +{ + var bounds = this.getLocalBounds(); + + var renderTexture = new PIXI.RenderTexture(bounds.width | 0, bounds.height | 0, renderer); + renderTexture.render(this, new PIXI.Point(-bounds.x, -bounds.y) ); + + return renderTexture; +}; + +PIXI.DisplayObject.prototype.updateCache = function() +{ + this._generateCachedSprite(); +}; + +PIXI.DisplayObject.prototype._renderCachedSprite = function(renderSession) +{ + if(renderSession.gl) + { + PIXI.Sprite.prototype._renderWebGL.call(this._cachedSprite, renderSession); + } + else + { + PIXI.Sprite.prototype._renderCanvas.call(this._cachedSprite, renderSession); + } +}; + +PIXI.DisplayObject.prototype._generateCachedSprite = function()//renderSession) +{ + this._cacheAsBitmap = false; + var bounds = this.getLocalBounds(); + + if(!this._cachedSprite) + { + var renderTexture = new PIXI.RenderTexture(bounds.width | 0, bounds.height | 0);//, renderSession.renderer); + + this._cachedSprite = new PIXI.Sprite(renderTexture); + this._cachedSprite.worldTransform = this.worldTransform; + } + else + { + this._cachedSprite.texture.resize(bounds.width | 0, bounds.height | 0); + } + + //REMOVE filter! + var tempFilters = this._filters; + this._filters = null; + + this._cachedSprite.filters = tempFilters; + this._cachedSprite.texture.render(this, new PIXI.Point(-bounds.x, -bounds.y) ); + + this._cachedSprite.anchor.x = -( bounds.x / bounds.width ); + this._cachedSprite.anchor.y = -( bounds.y / bounds.height ); + + this._filters = tempFilters; + + this._cacheAsBitmap = true; +}; + +/** +* Renders the object using the WebGL renderer +* +* @method _renderWebGL +* @param renderSession {RenderSession} +* @private +*/ +PIXI.DisplayObject.prototype._destroyCachedSprite = function() +{ + if(!this._cachedSprite)return; + + this._cachedSprite.texture.destroy(true); + // console.log("DESTROY") + // let the gc collect the unused sprite + // TODO could be object pooled! + this._cachedSprite = null; +}; + + +PIXI.DisplayObject.prototype._renderWebGL = function(renderSession) +{ + // OVERWRITE; + // this line is just here to pass jshinting :) + renderSession = renderSession; +}; + +/** +* Renders the object using the Canvas renderer +* +* @method _renderCanvas +* @param renderSession {RenderSession} +* @private +*/ +PIXI.DisplayObject.prototype._renderCanvas = function(renderSession) +{ + // OVERWRITE; + // this line is just here to pass jshinting :) + renderSession = renderSession; +}; + +/** + * The position of the displayObject on the x axis relative to the local coordinates of the parent. + * + * @property x + * @type Number + */ +Object.defineProperty(PIXI.DisplayObject.prototype, 'x', { + get: function() { + return this.position.x; + }, + set: function(value) { + this.position.x = value; + } +}); + +/** + * The position of the displayObject on the y axis relative to the local coordinates of the parent. + * + * @property y + * @type Number + */ +Object.defineProperty(PIXI.DisplayObject.prototype, 'y', { + get: function() { + return this.position.y; + }, + set: function(value) { + this.position.y = value; + } +}); + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + + +/** + * A DisplayObjectContainer represents a collection of display objects. + * It is the base class of all display objects that act as a container for other objects. + * + * @class DisplayObjectContainer + * @extends DisplayObject + * @constructor + */ +PIXI.DisplayObjectContainer = function() +{ + PIXI.DisplayObject.call( this ); + + /** + * [read-only] The array of children of this container. + * + * @property children + * @type Array + * @readOnly + */ + this.children = []; +}; + +// constructor +PIXI.DisplayObjectContainer.prototype = Object.create( PIXI.DisplayObject.prototype ); +PIXI.DisplayObjectContainer.prototype.constructor = PIXI.DisplayObjectContainer; + +/** + * The width of the displayObjectContainer, setting this will actually modify the scale to achieve the value set + * + * @property width + * @type Number + */ + + /* +Object.defineProperty(PIXI.DisplayObjectContainer.prototype, 'width', { + get: function() { + return this.scale.x * this.getLocalBounds().width; + }, + set: function(value) { + this.scale.x = value / (this.getLocalBounds().width/this.scale.x); + this._width = value; + } +}); +*/ + +/** + * The height of the displayObjectContainer, setting this will actually modify the scale to achieve the value set + * + * @property height + * @type Number + */ + +/* +Object.defineProperty(PIXI.DisplayObjectContainer.prototype, 'height', { + get: function() { + return this.scale.y * this.getLocalBounds().height; + }, + set: function(value) { + this.scale.y = value / (this.getLocalBounds().height/this.scale.y); + this._height = value; + } +}); +*/ + +/** + * Adds a child to the container. + * + * @method addChild + * @param child {DisplayObject} The DisplayObject to add to the container + */ +PIXI.DisplayObjectContainer.prototype.addChild = function(child) +{ + this.addChildAt(child, this.children.length); +}; + +/** + * Adds a child to the container at a specified index. If the index is out of bounds an error will be thrown + * + * @method addChildAt + * @param child {DisplayObject} The child to add + * @param index {Number} The index to place the child in + */ +PIXI.DisplayObjectContainer.prototype.addChildAt = function(child, index) +{ + if(index >= 0 && index <= this.children.length) + { + if(child.parent) + { + child.parent.removeChild(child); + } + + child.parent = this; + + this.children.splice(index, 0, child); + + if(this.stage)child.setStageReference(this.stage); + } + else + { + throw new Error(child + ' The index '+ index +' supplied is out of bounds ' + this.children.length); + } +}; + +/** + * [NYI] Swaps the depth of 2 displayObjects + * + * @method swapChildren + * @param child {DisplayObject} + * @param child2 {DisplayObject} + * @private + */ +PIXI.DisplayObjectContainer.prototype.swapChildren = function(child, child2) +{ + if(child === child2) { + return; + } + + var index1 = this.children.indexOf(child); + var index2 = this.children.indexOf(child2); + + if(index1 < 0 || index2 < 0) { + throw new Error('swapChildren: Both the supplied DisplayObjects must be a child of the caller.'); + } + + this.children[index1] = child2; + this.children[index2] = child; + +}; + +/** + * Returns the child at the specified index + * + * @method getChildAt + * @param index {Number} The index to get the child from + */ +PIXI.DisplayObjectContainer.prototype.getChildAt = function(index) +{ + if(index >= 0 && index < this.children.length) + { + return this.children[index]; + } + else + { + throw new Error('Supplied index does not exist in the child list, or the supplied DisplayObject must be a child of the caller'); + } +}; + +/** + * Removes a child from the container. + * + * @method removeChild + * @param child {DisplayObject} The DisplayObject to remove + */ +PIXI.DisplayObjectContainer.prototype.removeChild = function(child) +{ + return this.removeChildAt( this.children.indexOf( child ) ); +}; + +/** + * Removes a child from the specified index position in the child list of the container. + * + * @method removeChildAt + * @param index {Number} The index to get the child from + */ +PIXI.DisplayObjectContainer.prototype.removeChildAt = function(index) +{ + var child = this.getChildAt( index ); + if(this.stage) + child.removeStageReference(); + + child.parent = undefined; + this.children.splice( index, 1 ); + return child; +}; + +/** +* Removes all child instances from the child list of the container. +* +* @method removeChildren +* @param beginIndex {Number} The beginning position. Predefined value is 0. +* @param endIndex {Number} The ending position. Predefined value is children's array length. +*/ +PIXI.DisplayObjectContainer.prototype.removeChildren = function(beginIndex, endIndex) +{ + var begin = beginIndex || 0; + var end = typeof endIndex === 'number' ? endIndex : this.children.length; + var range = end - begin; + + if (range > 0 && range <= end) + { + var removed = this.children.splice(begin, range); + for (var i = 0; i < removed.length; i++) { + var child = removed[i]; + if(this.stage) + child.removeStageReference(); + child.parent = undefined; + } + return removed; + } + else + { + throw new Error( 'Range Error, numeric values are outside the acceptable range' ); + } +}; + +/* + * Updates the container's childrens transform for rendering + * + * @method updateTransform + * @private + */ +PIXI.DisplayObjectContainer.prototype.updateTransform = function() +{ + //this._currentBounds = null; + + if(!this.visible)return; + + PIXI.DisplayObject.prototype.updateTransform.call( this ); + + if(this._cacheAsBitmap)return; + + for(var i=0,j=this.children.length; i childMaxX ? maxX : childMaxX; + maxY = maxY > childMaxY ? maxY : childMaxY; + } + + if(!childVisible) + return PIXI.EmptyRectangle; + + var bounds = this._bounds; + + bounds.x = minX; + bounds.y = minY; + bounds.width = maxX - minX; + bounds.height = maxY - minY; + + // TODO: store a reference so that if this function gets called again in the render cycle we do not have to recalculate + //this._currentBounds = bounds; + + return bounds; +}; + +PIXI.DisplayObjectContainer.prototype.getLocalBounds = function() +{ + var matrixCache = this.worldTransform; + + this.worldTransform = PIXI.identityMatrix; + + for(var i=0,j=this.children.length; i maxX ? x1 : maxX; + maxX = x2 > maxX ? x2 : maxX; + maxX = x3 > maxX ? x3 : maxX; + maxX = x4 > maxX ? x4 : maxX; + + maxY = y1 > maxY ? y1 : maxY; + maxY = y2 > maxY ? y2 : maxY; + maxY = y3 > maxY ? y3 : maxY; + maxY = y4 > maxY ? y4 : maxY; + + var bounds = this._bounds; + + bounds.x = minX; + bounds.width = maxX - minX; + + bounds.y = minY; + bounds.height = maxY - minY; + + // store a reference so that if this function gets called again in the render cycle we do not have to recalculate + this._currentBounds = bounds; + + return bounds; +}; + +/** +* Renders the object using the WebGL renderer +* +* @method _renderWebGL +* @param renderSession {RenderSession} +* @private +*/ +PIXI.Sprite.prototype._renderWebGL = function(renderSession) +{ + // if the sprite is not visible or the alpha is 0 then no need to render this element + if(!this.visible || this.alpha <= 0)return; + + var i,j; + + // do a quick check to see if this element has a mask or a filter. + if(this._mask || this._filters) + { + var spriteBatch = renderSession.spriteBatch; + + if(this._mask) + { + spriteBatch.stop(); + renderSession.maskManager.pushMask(this.mask, renderSession); + spriteBatch.start(); + } + + if(this._filters) + { + spriteBatch.flush(); + renderSession.filterManager.pushFilter(this._filterBlock); + } + + // add this sprite to the batch + spriteBatch.render(this); + + // now loop through the children and make sure they get rendered + for(i=0,j=this.children.length; i} an array of {Texture} objects that make up the animation + */ +PIXI.MovieClip = function(textures) +{ + PIXI.Sprite.call(this, textures[0]); + + /** + * The array of textures that make up the animation + * + * @property textures + * @type Array + */ + this.textures = textures; + + /** + * The speed that the MovieClip will play at. Higher is faster, lower is slower + * + * @property animationSpeed + * @type Number + * @default 1 + */ + this.animationSpeed = 1; + + /** + * Whether or not the movie clip repeats after playing. + * + * @property loop + * @type Boolean + * @default true + */ + this.loop = true; + + /** + * Function to call when a MovieClip finishes playing + * + * @property onComplete + * @type Function + */ + this.onComplete = null; + + /** + * [read-only] The MovieClips current frame index (this may not have to be a whole number) + * + * @property currentFrame + * @type Number + * @default 0 + * @readOnly + */ + this.currentFrame = 0; + + /** + * [read-only] Indicates if the MovieClip is currently playing + * + * @property playing + * @type Boolean + * @readOnly + */ + this.playing = false; +}; + +// constructor +PIXI.MovieClip.prototype = Object.create( PIXI.Sprite.prototype ); +PIXI.MovieClip.prototype.constructor = PIXI.MovieClip; + +/** +* [read-only] totalFrames is the total number of frames in the MovieClip. This is the same as number of textures +* assigned to the MovieClip. +* +* @property totalFrames +* @type Number +* @default 0 +* @readOnly +*/ +Object.defineProperty( PIXI.MovieClip.prototype, 'totalFrames', { + get: function() { + + return this.textures.length; + } +}); + + +/** + * Stops the MovieClip + * + * @method stop + */ +PIXI.MovieClip.prototype.stop = function() +{ + this.playing = false; +}; + +/** + * Plays the MovieClip + * + * @method play + */ +PIXI.MovieClip.prototype.play = function() +{ + this.playing = true; +}; + +/** + * Stops the MovieClip and goes to a specific frame + * + * @method gotoAndStop + * @param frameNumber {Number} frame index to stop at + */ +PIXI.MovieClip.prototype.gotoAndStop = function(frameNumber) +{ + this.playing = false; + this.currentFrame = frameNumber; + var round = (this.currentFrame + 0.5) | 0; + this.setTexture(this.textures[round % this.textures.length]); +}; + +/** + * Goes to a specific frame and begins playing the MovieClip + * + * @method gotoAndPlay + * @param frameNumber {Number} frame index to start at + */ +PIXI.MovieClip.prototype.gotoAndPlay = function(frameNumber) +{ + this.currentFrame = frameNumber; + this.playing = true; +}; + +/* + * Updates the object transform for rendering + * + * @method updateTransform + * @private + */ +PIXI.MovieClip.prototype.updateTransform = function() +{ + PIXI.Sprite.prototype.updateTransform.call(this); + + if(!this.playing)return; + + this.currentFrame += this.animationSpeed; + + var round = (this.currentFrame + 0.5) | 0; + + if(this.loop || round < this.textures.length) + { + this.setTexture(this.textures[round % this.textures.length]); + } + else if(round >= this.textures.length) + { + this.gotoAndStop(this.textures.length - 1); + if(this.onComplete) + { + this.onComplete(); + } + } +}; + +/** + * A short hand way of creating a movieclip from an array of frame ids + * + * @static + * @method fromFrames + * @param frames {Array} the array of frames ids the movieclip will use as its texture frames + */ +PIXI.MovieClip.prototype.fromFrames = function(frames) +{ + var textures = []; + + for (var i = 0; i < frames.length; i++) + { + textures.push(new PIXI.Texture.fromFrame(frames[i])); + } + + return new PIXI.MovieClip(textures); +}; + +/** + * A short hand way of creating a movieclip from an array of image ids + * + * @static + * @method fromFrames + * @param frames {Array} the array of image ids the movieclip will use as its texture frames + */ +PIXI.MovieClip.prototype.fromImages = function(images) +{ + var textures = []; + + for (var i = 0; i < images.length; i++) + { + textures.push(new PIXI.Texture.fromImage(images[i])); + } + + return new PIXI.MovieClip(textures); +}; +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + + +PIXI.FilterBlock = function() +{ + this.visible = true; + this.renderable = true; +}; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + * - Modified by Tom Slezakowski http://www.tomslezakowski.com @TomSlezakowski (24/03/2014) - Added dropShadowColor. + */ + +/** + * A Text Object will create a line(s) of text. To split a line you can use '\n' + * or add a wordWrap property set to true and and wordWrapWidth property with a value + * in the style object + * + * @class Text + * @extends Sprite + * @constructor + * @param text {String} The copy that you would like the text to display + * @param [style] {Object} The style parameters + * @param [style.font] {String} default 'bold 20px Arial' The style and size of the font + * @param [style.fill='black'] {String|Number} A canvas fillstyle that will be used on the text e.g 'red', '#00FF00' + * @param [style.align='left'] {String} Alignment for multiline text ('left', 'center' or 'right'), does not affect single line text + * @param [style.stroke] {String|Number} A canvas fillstyle that will be used on the text stroke e.g 'blue', '#FCFF00' + * @param [style.strokeThickness=0] {Number} A number that represents the thickness of the stroke. Default is 0 (no stroke) + * @param [style.wordWrap=false] {Boolean} Indicates if word wrap should be used + * @param [style.wordWrapWidth=100] {Number} The width at which text will wrap, it needs wordWrap to be set to true + * @param [style.dropShadow=false] {Boolean} Set a drop shadow for the text + * @param [style.dropShadowColor='#000000'] {String} A fill style to be used on the dropshadow e.g 'red', '#00FF00' + * @param [style.dropShadowAngle=Math.PI/4] {Number} Set a angle of the drop shadow + * @param [style.dropShadowDistance=5] {Number} Set a distance of the drop shadow + */ +PIXI.Text = function(text, style) +{ + /** + * The canvas element that everything is drawn to + * + * @property canvas + * @type HTMLCanvasElement + */ + this.canvas = document.createElement('canvas'); + + /** + * The canvas 2d context that everything is drawn with + * @property context + * @type HTMLCanvasElement 2d Context + */ + this.context = this.canvas.getContext('2d'); + + PIXI.Sprite.call(this, PIXI.Texture.fromCanvas(this.canvas)); + + this.setText(text); + this.setStyle(style); + + this.updateText(); + this.dirty = false; +}; + +// constructor +PIXI.Text.prototype = Object.create(PIXI.Sprite.prototype); +PIXI.Text.prototype.constructor = PIXI.Text; + +/** + * Set the style of the text + * + * @method setStyle + * @param [style] {Object} The style parameters + * @param [style.font='bold 20pt Arial'] {String} The style and size of the font + * @param [style.fill='black'] {Object} A canvas fillstyle that will be used on the text eg 'red', '#00FF00' + * @param [style.align='left'] {String} Alignment for multiline text ('left', 'center' or 'right'), does not affect single line text + * @param [style.stroke='black'] {String} A canvas fillstyle that will be used on the text stroke eg 'blue', '#FCFF00' + * @param [style.strokeThickness=0] {Number} A number that represents the thickness of the stroke. Default is 0 (no stroke) + * @param [style.wordWrap=false] {Boolean} Indicates if word wrap should be used + * @param [style.wordWrapWidth=100] {Number} The width at which text will wrap + * @param [style.dropShadow=false] {Boolean} Set a drop shadow for the text + * @param [style.dropShadowColor='#000000'] {String} A fill style to be used on the dropshadow e.g 'red', '#00FF00' + * @param [style.dropShadowAngle=Math.PI/4] {Number} Set a angle of the drop shadow + * @param [style.dropShadowDistance=5] {Number} Set a distance of the drop shadow + */ +PIXI.Text.prototype.setStyle = function(style) +{ + style = style || {}; + style.font = style.font || 'bold 20pt Arial'; + style.fill = style.fill || 'black'; + style.align = style.align || 'left'; + style.stroke = style.stroke || 'black'; //provide a default, see: https://github.com/GoodBoyDigital/pixi.js/issues/136 + style.strokeThickness = style.strokeThickness || 0; + style.wordWrap = style.wordWrap || false; + style.wordWrapWidth = style.wordWrapWidth || 100; + style.wordWrapWidth = style.wordWrapWidth || 100; + + style.dropShadow = style.dropShadow || false; + style.dropShadowAngle = style.dropShadowAngle || Math.PI / 6; + style.dropShadowDistance = style.dropShadowDistance || 4; + style.dropShadowColor = style.dropShadowColor || 'black'; + + this.style = style; + this.dirty = true; +}; + +/** + * Set the copy for the text object. To split a line you can use '\n' + * + * @method setText + * @param {String} text The copy that you would like the text to display + */ +PIXI.Text.prototype.setText = function(text) +{ + this.text = text.toString() || ' '; + this.dirty = true; + +}; + +/** + * Renders text and updates it when needed + * + * @method updateText + * @private + */ +PIXI.Text.prototype.updateText = function() +{ + this.context.font = this.style.font; + + var outputText = this.text; + + // word wrap + // preserve original text + if(this.style.wordWrap)outputText = this.wordWrap(this.text); + + //split text into lines + var lines = outputText.split(/(?:\r\n|\r|\n)/); + + //calculate text width + var lineWidths = []; + var maxLineWidth = 0; + for (var i = 0; i < lines.length; i++) + { + var lineWidth = this.context.measureText(lines[i]).width; + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + + var width = maxLineWidth + this.style.strokeThickness; + if(this.style.dropShadow)width += this.style.dropShadowDistance; + + this.canvas.width = width + this.context.lineWidth; + //calculate text height + var lineHeight = this.determineFontHeight('font: ' + this.style.font + ';') + this.style.strokeThickness; + + var height = lineHeight * lines.length; + if(this.style.dropShadow)height += this.style.dropShadowDistance; + + this.canvas.height = height; + + if(navigator.isCocoonJS) this.context.clearRect(0,0,this.canvas.width,this.canvas.height); + + this.context.font = this.style.font; + this.context.strokeStyle = this.style.stroke; + this.context.lineWidth = this.style.strokeThickness; + this.context.textBaseline = 'top'; + + var linePositionX; + var linePositionY; + + if(this.style.dropShadow) + { + this.context.fillStyle = this.style.dropShadowColor; + + var xShadowOffset = Math.sin(this.style.dropShadowAngle) * this.style.dropShadowDistance; + var yShadowOffset = Math.cos(this.style.dropShadowAngle) * this.style.dropShadowDistance; + + for (i = 0; i < lines.length; i++) + { + linePositionX = this.style.strokeThickness / 2; + linePositionY = this.style.strokeThickness / 2 + i * lineHeight; + + if(this.style.align === 'right') + { + linePositionX += maxLineWidth - lineWidths[i]; + } + else if(this.style.align === 'center') + { + linePositionX += (maxLineWidth - lineWidths[i]) / 2; + } + + if(this.style.fill) + { + this.context.fillText(lines[i], linePositionX + xShadowOffset, linePositionY + yShadowOffset); + } + + // if(dropShadow) + } + } + + //set canvas text styles + this.context.fillStyle = this.style.fill; + + //draw lines line by line + for (i = 0; i < lines.length; i++) + { + linePositionX = this.style.strokeThickness / 2; + linePositionY = this.style.strokeThickness / 2 + i * lineHeight; + + if(this.style.align === 'right') + { + linePositionX += maxLineWidth - lineWidths[i]; + } + else if(this.style.align === 'center') + { + linePositionX += (maxLineWidth - lineWidths[i]) / 2; + } + + if(this.style.stroke && this.style.strokeThickness) + { + this.context.strokeText(lines[i], linePositionX, linePositionY); + } + + if(this.style.fill) + { + this.context.fillText(lines[i], linePositionX, linePositionY); + } + + // if(dropShadow) + } + + + this.updateTexture(); +}; + +/** + * Updates texture size based on canvas size + * + * @method updateTexture + * @private + */ +PIXI.Text.prototype.updateTexture = function() +{ + this.texture.baseTexture.width = this.canvas.width; + this.texture.baseTexture.height = this.canvas.height; + this.texture.frame.width = this.canvas.width; + this.texture.frame.height = this.canvas.height; + + this._width = this.canvas.width; + this._height = this.canvas.height; + + this.requiresUpdate = true; +}; + +/** +* Renders the object using the WebGL renderer +* +* @method _renderWebGL +* @param renderSession {RenderSession} +* @private +*/ +PIXI.Text.prototype._renderWebGL = function(renderSession) +{ + if(this.requiresUpdate) + { + this.requiresUpdate = false; + PIXI.updateWebGLTexture(this.texture.baseTexture, renderSession.gl); + } + + PIXI.Sprite.prototype._renderWebGL.call(this, renderSession); +}; + +/** + * Updates the transform of this object + * + * @method updateTransform + * @private + */ +PIXI.Text.prototype.updateTransform = function() +{ + if(this.dirty) + { + this.updateText(); + this.dirty = false; + } + + PIXI.Sprite.prototype.updateTransform.call(this); +}; + +/* + * http://stackoverflow.com/users/34441/ellisbben + * great solution to the problem! + * returns the height of the given font + * + * @method determineFontHeight + * @param fontStyle {Object} + * @private + */ +PIXI.Text.prototype.determineFontHeight = function(fontStyle) +{ + // build a little reference dictionary so if the font style has been used return a + // cached version... + var result = PIXI.Text.heightCache[fontStyle]; + + if(!result) + { + var body = document.getElementsByTagName('body')[0]; + var dummy = document.createElement('div'); + var dummyText = document.createTextNode('M'); + dummy.appendChild(dummyText); + dummy.setAttribute('style', fontStyle + ';position:absolute;top:0;left:0'); + body.appendChild(dummy); + + result = dummy.offsetHeight; + PIXI.Text.heightCache[fontStyle] = result; + + body.removeChild(dummy); + } + + return result; +}; + +/** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @method wordWrap + * @param text {String} + * @private + */ +PIXI.Text.prototype.wordWrap = function(text) +{ + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + var result = ''; + var lines = text.split('\n'); + for (var i = 0; i < lines.length; i++) + { + var spaceLeft = this.style.wordWrapWidth; + var words = lines[i].split(' '); + for (var j = 0; j < words.length; j++) + { + var wordWidth = this.context.measureText(words[j]).width; + var wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; + if(j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if(j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = this.style.wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ' ' + words[j]; + } + } + + if (i < lines.length-1) + { + result += '\n'; + } + } + return result; +}; + +/** + * Destroys this text object + * + * @method destroy + * @param destroyTexture {Boolean} + */ +PIXI.Text.prototype.destroy = function(destroyTexture) +{ + if(destroyTexture) + { + this.texture.destroy(); + } + +}; + +PIXI.Text.heightCache = {}; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +/** + * A Text Object will create a line(s) of text using bitmap font. To split a line you can use '\n', '\r' or '\r\n' + * You can generate the fnt files using + * http://www.angelcode.com/products/bmfont/ for windows or + * http://www.bmglyph.com/ for mac. + * + * @class BitmapText + * @extends DisplayObjectContainer + * @constructor + * @param text {String} The copy that you would like the text to display + * @param style {Object} The style parameters + * @param style.font {String} The size (optional) and bitmap font id (required) eq 'Arial' or '20px Arial' (must have loaded previously) + * @param [style.align='left'] {String} Alignment for multiline text ('left', 'center' or 'right'), does not affect single line text + */ +PIXI.BitmapText = function(text, style) +{ + PIXI.DisplayObjectContainer.call(this); + + this._pool = []; + + this.setText(text); + this.setStyle(style); + this.updateText(); + this.dirty = false; +}; + +// constructor +PIXI.BitmapText.prototype = Object.create(PIXI.DisplayObjectContainer.prototype); +PIXI.BitmapText.prototype.constructor = PIXI.BitmapText; + +/** + * Set the copy for the text object + * + * @method setText + * @param text {String} The copy that you would like the text to display + */ +PIXI.BitmapText.prototype.setText = function(text) +{ + this.text = text || ' '; + this.dirty = true; +}; + +/** + * Set the style of the text + * style.font {String} The size (optional) and bitmap font id (required) eq 'Arial' or '20px Arial' (must have loaded previously) + * [style.align='left'] {String} Alignment for multiline text ('left', 'center' or 'right'), does not affect single line text + * + * @method setStyle + * @param style {Object} The style parameters, contained as properties of an object + */ +PIXI.BitmapText.prototype.setStyle = function(style) +{ + style = style || {}; + style.align = style.align || 'left'; + this.style = style; + + var font = style.font.split(' '); + this.fontName = font[font.length - 1]; + this.fontSize = font.length >= 2 ? parseInt(font[font.length - 2], 10) : PIXI.BitmapText.fonts[this.fontName].size; + + this.dirty = true; + this.tint = style.tint; +}; + +/** + * Renders text and updates it when needed + * + * @method updateText + * @private + */ +PIXI.BitmapText.prototype.updateText = function() +{ + var data = PIXI.BitmapText.fonts[this.fontName]; + var pos = new PIXI.Point(); + var prevCharCode = null; + var chars = []; + var maxLineWidth = 0; + var lineWidths = []; + var line = 0; + var scale = this.fontSize / data.size; + + + for(var i = 0; i < this.text.length; i++) + { + var charCode = this.text.charCodeAt(i); + if(/(?:\r\n|\r|\n)/.test(this.text.charAt(i))) + { + lineWidths.push(pos.x); + maxLineWidth = Math.max(maxLineWidth, pos.x); + line++; + + pos.x = 0; + pos.y += data.lineHeight; + prevCharCode = null; + continue; + } + + var charData = data.chars[charCode]; + if(!charData) continue; + + if(prevCharCode && charData[prevCharCode]) + { + pos.x += charData.kerning[prevCharCode]; + } + chars.push({texture:charData.texture, line: line, charCode: charCode, position: new PIXI.Point(pos.x + charData.xOffset, pos.y + charData.yOffset)}); + pos.x += charData.xAdvance; + + prevCharCode = charCode; + } + + lineWidths.push(pos.x); + maxLineWidth = Math.max(maxLineWidth, pos.x); + + var lineAlignOffsets = []; + for(i = 0; i <= line; i++) + { + var alignOffset = 0; + if(this.style.align === 'right') + { + alignOffset = maxLineWidth - lineWidths[i]; + } + else if(this.style.align === 'center') + { + alignOffset = (maxLineWidth - lineWidths[i]) / 2; + } + lineAlignOffsets.push(alignOffset); + } + + var lenChildren = this.children.length; + var lenChars = chars.length; + var tint = this.tint || 0xFFFFFF; + for(i = 0; i < lenChars; i++) + { + var c = i < lenChildren ? this.children[i] : this._pool.pop(); // get old child if have. if not - take from pool. + + if (c) c.setTexture(chars[i].texture); // check if got one before. + else c = new PIXI.Sprite(chars[i].texture); // if no create new one. + + c.position.x = (chars[i].position.x + lineAlignOffsets[chars[i].line]) * scale; + c.position.y = chars[i].position.y * scale; + c.scale.x = c.scale.y = scale; + c.tint = tint; + if (!c.parent) this.addChild(c); + } + + // remove unnecessary children. + // and put their into the pool. + while(this.children.length > lenChars) + { + var child = this.getChildAt(this.children.length - 1); + this._pool.push(child); + this.removeChild(child); + } + + + /** + * [read-only] The width of the overall text, different from fontSize, + * which is defined in the style object + * + * @property textWidth + * @type Number + */ + this.textWidth = maxLineWidth * scale; + + /** + * [read-only] The height of the overall text, different from fontSize, + * which is defined in the style object + * + * @property textHeight + * @type Number + */ + this.textHeight = (pos.y + data.lineHeight) * scale; +}; + +/** + * Updates the transform of this object + * + * @method updateTransform + * @private + */ +PIXI.BitmapText.prototype.updateTransform = function() +{ + if(this.dirty) + { + this.updateText(); + this.dirty = false; + } + + PIXI.DisplayObjectContainer.prototype.updateTransform.call(this); +}; + +PIXI.BitmapText.fonts = {}; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +/** + * Holds all information related to an Interaction event + * + * @class InteractionData + * @constructor + */ +PIXI.InteractionData = function() +{ + /** + * This point stores the global coords of where the touch/mouse event happened + * + * @property global + * @type Point + */ + this.global = new PIXI.Point(); + + + /** + * The target Sprite that was interacted with + * + * @property target + * @type Sprite + */ + this.target = null; + + /** + * When passed to an event handler, this will be the original DOM Event that was captured + * + * @property originalEvent + * @type Event + */ + this.originalEvent = null; +}; + +/** + * This will return the local coordinates of the specified displayObject for this InteractionData + * + * @method getLocalPosition + * @param displayObject {DisplayObject} The DisplayObject that you would like the local coords off + * @return {Point} A point containing the coordinates of the InteractionData position relative to the DisplayObject + */ +PIXI.InteractionData.prototype.getLocalPosition = function(displayObject) +{ + var worldTransform = displayObject.worldTransform; + var global = this.global; + + // do a cheeky transform to get the mouse coords; + var a00 = worldTransform.a, a01 = worldTransform.b, a02 = worldTransform.tx, + a10 = worldTransform.c, a11 = worldTransform.d, a12 = worldTransform.ty, + id = 1 / (a00 * a11 + a01 * -a10); + // set the mouse coords... + return new PIXI.Point(a11 * id * global.x + -a01 * id * global.y + (a12 * a01 - a02 * a11) * id, + a00 * id * global.y + -a10 * id * global.x + (-a12 * a00 + a02 * a10) * id); +}; + +// constructor +PIXI.InteractionData.prototype.constructor = PIXI.InteractionData; +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + + /** + * The interaction manager deals with mouse and touch events. Any DisplayObject can be interactive + * if its interactive parameter is set to true + * This manager also supports multitouch. + * + * @class InteractionManager + * @constructor + * @param stage {Stage} The stage to handle interactions + */ +PIXI.InteractionManager = function(stage) +{ + /** + * a reference to the stage + * + * @property stage + * @type Stage + */ + this.stage = stage; + + /** + * the mouse data + * + * @property mouse + * @type InteractionData + */ + this.mouse = new PIXI.InteractionData(); + + /** + * an object that stores current touches (InteractionData) by id reference + * + * @property touchs + * @type Object + */ + this.touchs = {}; + + // helpers + this.tempPoint = new PIXI.Point(); + + /** + * + * @property mouseoverEnabled + * @type Boolean + * @default + */ + this.mouseoverEnabled = true; + + /** + * tiny little interactiveData pool ! + * + * @property pool + * @type Array + */ + this.pool = []; + + /** + * An array containing all the iterative items from the our interactive tree + * @property interactiveItems + * @type Array + * @private + * + */ + this.interactiveItems = []; + + /** + * Our canvas + * @property interactionDOMElement + * @type HTMLCanvasElement + * @private + */ + this.interactionDOMElement = null; + + //this will make it so that you dont have to call bind all the time + this.onMouseMove = this.onMouseMove.bind( this ); + this.onMouseDown = this.onMouseDown.bind(this); + this.onMouseOut = this.onMouseOut.bind(this); + this.onMouseUp = this.onMouseUp.bind(this); + + this.onTouchStart = this.onTouchStart.bind(this); + this.onTouchEnd = this.onTouchEnd.bind(this); + this.onTouchMove = this.onTouchMove.bind(this); + + this.last = 0; + + /** + * The css style of the cursor that is being used + * @property currentCursorStyle + * @type String + * + */ + this.currentCursorStyle = 'inherit'; + + /** + * Is set to true when the mouse is moved out of the canvas + * @property mouseOut + * @type Boolean + * + */ + this.mouseOut = false; +}; + +// constructor +PIXI.InteractionManager.prototype.constructor = PIXI.InteractionManager; + +/** + * Collects an interactive sprite recursively to have their interactions managed + * + * @method collectInteractiveSprite + * @param displayObject {DisplayObject} the displayObject to collect + * @param iParent {DisplayObject} the display object's parent + * @private + */ +PIXI.InteractionManager.prototype.collectInteractiveSprite = function(displayObject, iParent) +{ + var children = displayObject.children; + var length = children.length; + + // make an interaction tree... {item.__interactiveParent} + for (var i = length-1; i >= 0; i--) + { + var child = children[i]; + + // push all interactive bits + if(child._interactive) + { + iParent.interactiveChildren = true; + //child.__iParent = iParent; + this.interactiveItems.push(child); + + if(child.children.length > 0) + { + this.collectInteractiveSprite(child, child); + } + } + else + { + child.__iParent = null; + + if(child.children.length > 0) + { + this.collectInteractiveSprite(child, iParent); + } + } + + } +}; + +/** + * Sets the target for event delegation + * + * @method setTarget + * @param target {WebGLRenderer|CanvasRenderer} the renderer to bind events to + * @private + */ +PIXI.InteractionManager.prototype.setTarget = function(target) +{ + this.target = target; + + //check if the dom element has been set. If it has don't do anything + if( this.interactionDOMElement === null ) { + + this.setTargetDomElement( target.view ); + } + + +}; + + +/** + * Sets the DOM element which will receive mouse/touch events. This is useful for when you have other DOM + * elements on top of the renderers Canvas element. With this you'll be able to delegate another DOM element + * to receive those events + * + * @method setTargetDomElement + * @param domElement {DOMElement} the DOM element which will receive mouse and touch events + * @private + */ +PIXI.InteractionManager.prototype.setTargetDomElement = function(domElement) +{ + + this.removeEvents(); + + + if (window.navigator.msPointerEnabled) + { + // time to remove some of that zoom in ja.. + domElement.style['-ms-content-zooming'] = 'none'; + domElement.style['-ms-touch-action'] = 'none'; + + // DO some window specific touch! + } + + this.interactionDOMElement = domElement; + + domElement.addEventListener('mousemove', this.onMouseMove, true); + domElement.addEventListener('mousedown', this.onMouseDown, true); + domElement.addEventListener('mouseout', this.onMouseOut, true); + + // aint no multi touch just yet! + domElement.addEventListener('touchstart', this.onTouchStart, true); + domElement.addEventListener('touchend', this.onTouchEnd, true); + domElement.addEventListener('touchmove', this.onTouchMove, true); + + window.addEventListener('mouseup', this.onMouseUp, true); +}; + + +PIXI.InteractionManager.prototype.removeEvents = function() +{ + if(!this.interactionDOMElement)return; + + this.interactionDOMElement.style['-ms-content-zooming'] = ''; + this.interactionDOMElement.style['-ms-touch-action'] = ''; + + this.interactionDOMElement.removeEventListener('mousemove', this.onMouseMove, true); + this.interactionDOMElement.removeEventListener('mousedown', this.onMouseDown, true); + this.interactionDOMElement.removeEventListener('mouseout', this.onMouseOut, true); + + // aint no multi touch just yet! + this.interactionDOMElement.removeEventListener('touchstart', this.onTouchStart, true); + this.interactionDOMElement.removeEventListener('touchend', this.onTouchEnd, true); + this.interactionDOMElement.removeEventListener('touchmove', this.onTouchMove, true); + + this.interactionDOMElement = null; + + window.removeEventListener('mouseup', this.onMouseUp, true); +}; + +/** + * updates the state of interactive objects + * + * @method update + * @private + */ +PIXI.InteractionManager.prototype.update = function() +{ + if(!this.target)return; + + // frequency of 30fps?? + var now = Date.now(); + var diff = now - this.last; + diff = (diff * PIXI.INTERACTION_FREQUENCY ) / 1000; + if(diff < 1)return; + this.last = now; + + var i = 0; + + // ok.. so mouse events?? + // yes for now :) + // OPTIMISE - how often to check?? + if(this.dirty) + { + this.dirty = false; + + var len = this.interactiveItems.length; + + for (i = 0; i < len; i++) { + this.interactiveItems[i].interactiveChildren = false; + } + + this.interactiveItems = []; + + if(this.stage.interactive)this.interactiveItems.push(this.stage); + // go through and collect all the objects that are interactive.. + this.collectInteractiveSprite(this.stage, this.stage); + } + + // loop through interactive objects! + var length = this.interactiveItems.length; + var cursor = 'inherit'; + var over = false; + + for (i = 0; i < length; i++) + { + var item = this.interactiveItems[i]; + + // OPTIMISATION - only calculate every time if the mousemove function exists.. + // OK so.. does the object have any other interactive functions? + // hit-test the clip! + // if(item.mouseover || item.mouseout || item.buttonMode) + // { + // ok so there are some functions so lets hit test it.. + item.__hit = this.hitTest(item, this.mouse); + this.mouse.target = item; + // ok so deal with interactions.. + // looks like there was a hit! + if(item.__hit && !over) + { + if(item.buttonMode) cursor = item.defaultCursor; + + if(!item.interactiveChildren)over = true; + + if(!item.__isOver) + { + if(item.mouseover)item.mouseover(this.mouse); + item.__isOver = true; + } + } + else + { + if(item.__isOver) + { + // roll out! + if(item.mouseout)item.mouseout(this.mouse); + item.__isOver = false; + } + } + } + + if( this.currentCursorStyle !== cursor ) + { + this.currentCursorStyle = cursor; + this.interactionDOMElement.style.cursor = cursor; + } +}; + +/** + * Is called when the mouse moves across the renderer element + * + * @method onMouseMove + * @param event {Event} The DOM event of the mouse moving + * @private + */ +PIXI.InteractionManager.prototype.onMouseMove = function(event) +{ + this.mouse.originalEvent = event || window.event; //IE uses window.event + // TODO optimize by not check EVERY TIME! maybe half as often? // + var rect = this.interactionDOMElement.getBoundingClientRect(); + + this.mouse.global.x = (event.clientX - rect.left) * (this.target.width / rect.width); + this.mouse.global.y = (event.clientY - rect.top) * ( this.target.height / rect.height); + + var length = this.interactiveItems.length; + + for (var i = 0; i < length; i++) + { + var item = this.interactiveItems[i]; + + if(item.mousemove) + { + //call the function! + item.mousemove(this.mouse); + } + } +}; + +/** + * Is called when the mouse button is pressed down on the renderer element + * + * @method onMouseDown + * @param event {Event} The DOM event of a mouse button being pressed down + * @private + */ +PIXI.InteractionManager.prototype.onMouseDown = function(event) +{ + this.mouse.originalEvent = event || window.event; //IE uses window.event + + if(PIXI.AUTO_PREVENT_DEFAULT)this.mouse.originalEvent.preventDefault(); + + // loop through interaction tree... + // hit test each item! -> + // get interactive items under point?? + //stage.__i + var length = this.interactiveItems.length; + + // while + // hit test + for (var i = 0; i < length; i++) + { + var item = this.interactiveItems[i]; + + if(item.mousedown || item.click) + { + item.__mouseIsDown = true; + item.__hit = this.hitTest(item, this.mouse); + + if(item.__hit) + { + //call the function! + if(item.mousedown)item.mousedown(this.mouse); + item.__isDown = true; + + // just the one! + if(!item.interactiveChildren)break; + } + } + } +}; + +/** + * Is called when the mouse button is moved out of the renderer element + * + * @method onMouseOut + * @param event {Event} The DOM event of a mouse button being moved out + * @private + */ +PIXI.InteractionManager.prototype.onMouseOut = function() +{ + var length = this.interactiveItems.length; + + this.interactionDOMElement.style.cursor = 'inherit'; + + for (var i = 0; i < length; i++) + { + var item = this.interactiveItems[i]; + if(item.__isOver) + { + this.mouse.target = item; + if(item.mouseout)item.mouseout(this.mouse); + item.__isOver = false; + } + } + + this.mouseOut = true; + + // move the mouse to an impossible position + this.mouse.global.x = -10000; + this.mouse.global.y = -10000; +}; + +/** + * Is called when the mouse button is released on the renderer element + * + * @method onMouseUp + * @param event {Event} The DOM event of a mouse button being released + * @private + */ +PIXI.InteractionManager.prototype.onMouseUp = function(event) +{ + + this.mouse.originalEvent = event || window.event; //IE uses window.event + + var length = this.interactiveItems.length; + var up = false; + + for (var i = 0; i < length; i++) + { + var item = this.interactiveItems[i]; + + item.__hit = this.hitTest(item, this.mouse); + + if(item.__hit && !up) + { + //call the function! + if(item.mouseup) + { + item.mouseup(this.mouse); + } + if(item.__isDown) + { + if(item.click)item.click(this.mouse); + } + + if(!item.interactiveChildren)up = true; + } + else + { + if(item.__isDown) + { + if(item.mouseupoutside)item.mouseupoutside(this.mouse); + } + } + + item.__isDown = false; + //} + } +}; + +/** + * Tests if the current mouse coordinates hit a sprite + * + * @method hitTest + * @param item {DisplayObject} The displayObject to test for a hit + * @param interactionData {InteractionData} The interactionData object to update in the case there is a hit + * @private + */ +PIXI.InteractionManager.prototype.hitTest = function(item, interactionData) +{ + var global = interactionData.global; + + if( !item.worldVisible )return false; + + // temp fix for if the element is in a non visible + + var isSprite = (item instanceof PIXI.Sprite), + worldTransform = item.worldTransform, + a00 = worldTransform.a, a01 = worldTransform.b, a02 = worldTransform.tx, + a10 = worldTransform.c, a11 = worldTransform.d, a12 = worldTransform.ty, + id = 1 / (a00 * a11 + a01 * -a10), + x = a11 * id * global.x + -a01 * id * global.y + (a12 * a01 - a02 * a11) * id, + y = a00 * id * global.y + -a10 * id * global.x + (-a12 * a00 + a02 * a10) * id; + + interactionData.target = item; + + //a sprite or display object with a hit area defined + if(item.hitArea && item.hitArea.contains) { + if(item.hitArea.contains(x, y)) { + //if(isSprite) + interactionData.target = item; + + return true; + } + + return false; + } + // a sprite with no hitarea defined + else if(isSprite) + { + var width = item.texture.frame.width, + height = item.texture.frame.height, + x1 = -width * item.anchor.x, + y1; + + if(x > x1 && x < x1 + width) + { + y1 = -height * item.anchor.y; + + if(y > y1 && y < y1 + height) + { + // set the target property if a hit is true! + interactionData.target = item; + return true; + } + } + } + + var length = item.children.length; + + for (var i = 0; i < length; i++) + { + var tempItem = item.children[i]; + var hit = this.hitTest(tempItem, interactionData); + if(hit) + { + // hmm.. TODO SET CORRECT TARGET? + interactionData.target = item; + return true; + } + } + + return false; +}; + +/** + * Is called when a touch is moved across the renderer element + * + * @method onTouchMove + * @param event {Event} The DOM event of a touch moving across the renderer view + * @private + */ +PIXI.InteractionManager.prototype.onTouchMove = function(event) +{ + var rect = this.interactionDOMElement.getBoundingClientRect(); + var changedTouches = event.changedTouches; + var touchData; + var i = 0; + + for (i = 0; i < changedTouches.length; i++) + { + var touchEvent = changedTouches[i]; + touchData = this.touchs[touchEvent.identifier]; + touchData.originalEvent = event || window.event; + + // update the touch position + touchData.global.x = (touchEvent.clientX - rect.left) * (this.target.width / rect.width); + touchData.global.y = (touchEvent.clientY - rect.top) * (this.target.height / rect.height); + if(navigator.isCocoonJS) { + touchData.global.x = touchEvent.clientX; + touchData.global.y = touchEvent.clientY; + } + + for (var j = 0; j < this.interactiveItems.length; j++) + { + var item = this.interactiveItems[j]; + if(item.touchmove && item.__touchData[touchEvent.identifier]) item.touchmove(touchData); + } + } +}; + +/** + * Is called when a touch is started on the renderer element + * + * @method onTouchStart + * @param event {Event} The DOM event of a touch starting on the renderer view + * @private + */ +PIXI.InteractionManager.prototype.onTouchStart = function(event) +{ + var rect = this.interactionDOMElement.getBoundingClientRect(); + + if(PIXI.AUTO_PREVENT_DEFAULT)event.preventDefault(); + + var changedTouches = event.changedTouches; + for (var i=0; i < changedTouches.length; i++) + { + var touchEvent = changedTouches[i]; + + var touchData = this.pool.pop(); + if(!touchData)touchData = new PIXI.InteractionData(); + + touchData.originalEvent = event || window.event; + + this.touchs[touchEvent.identifier] = touchData; + touchData.global.x = (touchEvent.clientX - rect.left) * (this.target.width / rect.width); + touchData.global.y = (touchEvent.clientY - rect.top) * (this.target.height / rect.height); + if(navigator.isCocoonJS) { + touchData.global.x = touchEvent.clientX; + touchData.global.y = touchEvent.clientY; + } + + var length = this.interactiveItems.length; + + for (var j = 0; j < length; j++) + { + var item = this.interactiveItems[j]; + + if(item.touchstart || item.tap) + { + item.__hit = this.hitTest(item, touchData); + + if(item.__hit) + { + //call the function! + if(item.touchstart)item.touchstart(touchData); + item.__isDown = true; + item.__touchData = item.__touchData || {}; + item.__touchData[touchEvent.identifier] = touchData; + + if(!item.interactiveChildren)break; + } + } + } + } +}; + +/** + * Is called when a touch is ended on the renderer element + * + * @method onTouchEnd + * @param event {Event} The DOM event of a touch ending on the renderer view + * @private + */ +PIXI.InteractionManager.prototype.onTouchEnd = function(event) +{ + //this.mouse.originalEvent = event || window.event; //IE uses window.event + var rect = this.interactionDOMElement.getBoundingClientRect(); + var changedTouches = event.changedTouches; + + for (var i=0; i < changedTouches.length; i++) + { + var touchEvent = changedTouches[i]; + var touchData = this.touchs[touchEvent.identifier]; + var up = false; + touchData.global.x = (touchEvent.clientX - rect.left) * (this.target.width / rect.width); + touchData.global.y = (touchEvent.clientY - rect.top) * (this.target.height / rect.height); + if(navigator.isCocoonJS) { + touchData.global.x = touchEvent.clientX; + touchData.global.y = touchEvent.clientY; + } + + var length = this.interactiveItems.length; + for (var j = 0; j < length; j++) + { + var item = this.interactiveItems[j]; + + if(item.__touchData && item.__touchData[touchEvent.identifier]) { + + item.__hit = this.hitTest(item, item.__touchData[touchEvent.identifier]); + + // so this one WAS down... + touchData.originalEvent = event || window.event; + // hitTest?? + + if(item.touchend || item.tap) + { + if(item.__hit && !up) + { + if(item.touchend)item.touchend(touchData); + if(item.__isDown) + { + if(item.tap)item.tap(touchData); + } + + if(!item.interactiveChildren)up = true; + } + else + { + if(item.__isDown) + { + if(item.touchendoutside)item.touchendoutside(touchData); + } + } + + item.__isDown = false; + } + + item.__touchData[touchEvent.identifier] = null; + } + } + // remove the touch.. + this.pool.push(touchData); + this.touchs[touchEvent.identifier] = null; + } +}; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +/** + * A Stage represents the root of the display tree. Everything connected to the stage is rendered + * + * @class Stage + * @extends DisplayObjectContainer + * @constructor + * @param backgroundColor {Number} the background color of the stage, you have to pass this in is in hex format + * like: 0xFFFFFF for white + * + * Creating a stage is a mandatory process when you use Pixi, which is as simple as this : + * var stage = new PIXI.Stage(0xFFFFFF); + * where the parameter given is the background colour of the stage, in hex + * you will use this stage instance to add your sprites to it and therefore to the renderer + * Here is how to add a sprite to the stage : + * stage.addChild(sprite); + */ +PIXI.Stage = function(backgroundColor) +{ + PIXI.DisplayObjectContainer.call( this ); + + /** + * [read-only] Current transform of the object based on world (parent) factors + * + * @property worldTransform + * @type Mat3 + * @readOnly + * @private + */ + this.worldTransform = new PIXI.Matrix(); + + /** + * Whether or not the stage is interactive + * + * @property interactive + * @type Boolean + */ + this.interactive = true; + + /** + * The interaction manage for this stage, manages all interactive activity on the stage + * + * @property interactive + * @type InteractionManager + */ + this.interactionManager = new PIXI.InteractionManager(this); + + /** + * Whether the stage is dirty and needs to have interactions updated + * + * @property dirty + * @type Boolean + * @private + */ + this.dirty = true; + + //the stage is its own stage + this.stage = this; + + //optimize hit detection a bit + this.stage.hitArea = new PIXI.Rectangle(0,0,100000, 100000); + + this.setBackgroundColor(backgroundColor); +}; + +// constructor +PIXI.Stage.prototype = Object.create( PIXI.DisplayObjectContainer.prototype ); +PIXI.Stage.prototype.constructor = PIXI.Stage; + +/** + * Sets another DOM element which can receive mouse/touch interactions instead of the default Canvas element. + * This is useful for when you have other DOM elements on top of the Canvas element. + * + * @method setInteractionDelegate + * @param domElement {DOMElement} This new domElement which will receive mouse/touch events + */ +PIXI.Stage.prototype.setInteractionDelegate = function(domElement) +{ + this.interactionManager.setTargetDomElement( domElement ); +}; + +/* + * Updates the object transform for rendering + * + * @method updateTransform + * @private + */ +PIXI.Stage.prototype.updateTransform = function() +{ + this.worldAlpha = 1; + + for(var i=0,j=this.children.length; i> 16 & 0xFF) / 255, ( hex >> 8 & 0xFF) / 255, (hex & 0xFF)/ 255]; +}; + +/** + * Converts a color as an [R, G, B] array to a hex number + * + * @method rgb2hex + * @param rgb {Array} + */ +PIXI.rgb2hex = function(rgb) { + return ((rgb[0]*255 << 16) + (rgb[1]*255 << 8) + rgb[2]*255); +}; + +/** + * A polyfill for Function.prototype.bind + * + * @method bind + */ +if (typeof Function.prototype.bind !== 'function') { + Function.prototype.bind = (function () { + var slice = Array.prototype.slice; + return function (thisArg) { + var target = this, boundArgs = slice.call(arguments, 1); + + if (typeof target !== 'function') throw new TypeError(); + + function bound() { + var args = boundArgs.concat(slice.call(arguments)); + target.apply(this instanceof bound ? this : thisArg, args); + } + + bound.prototype = (function F(proto) { + if (proto) F.prototype = proto; + if (!(this instanceof F)) return new F(); + })(target.prototype); + + return bound; + }; + })(); +} + +/** + * A wrapper for ajax requests to be handled cross browser + * + * @class AjaxRequest + * @constructor + */ +PIXI.AjaxRequest = function() +{ + var activexmodes = ['Msxml2.XMLHTTP.6.0', 'Msxml2.XMLHTTP.3.0', 'Microsoft.XMLHTTP']; //activeX versions to check for in IE + + if (window.ActiveXObject) + { //Test for support for ActiveXObject in IE first (as XMLHttpRequest in IE7 is broken) + for (var i=0; i 0 && (number & (number - 1)) === 0) // see: http://goo.gl/D9kPj + return number; + else + { + var result = 1; + while (result < number) result <<= 1; + return result; + } +}; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +/** + * https://github.com/mrdoob/eventtarget.js/ + * THankS mr DOob! + */ + +/** + * Adds event emitter functionality to a class + * + * @class EventTarget + * @example + * function MyEmitter() { + * PIXI.EventTarget.call(this); //mixes in event target stuff + * } + * + * var em = new MyEmitter(); + * em.emit({ type: 'eventName', data: 'some data' }); + */ +PIXI.EventTarget = function () { + + /** + * Holds all the listeners + * + * @property listeners + * @type Object + */ + var listeners = {}; + + /** + * Adds a listener for a specific event + * + * @method addEventListener + * @param type {string} A string representing the event type to listen for. + * @param listener {function} The callback function that will be fired when the event occurs + */ + this.addEventListener = this.on = function ( type, listener ) { + + + if ( listeners[ type ] === undefined ) { + + listeners[ type ] = []; + + } + + if ( listeners[ type ].indexOf( listener ) === - 1 ) { + + listeners[ type ].push( listener ); + } + + }; + + /** + * Fires the event, ie pretends that the event has happened + * + * @method dispatchEvent + * @param event {Event} the event object + */ + this.dispatchEvent = this.emit = function ( event ) { + + if ( !listeners[ event.type ] || !listeners[ event.type ].length ) { + + return; + + } + + for(var i = 0, l = listeners[ event.type ].length; i < l; i++) { + + listeners[ event.type ][ i ]( event ); + + } + + }; + + /** + * Removes the specified listener that was assigned to the specified event type + * + * @method removeEventListener + * @param type {string} A string representing the event type which will have its listener removed + * @param listener {function} The callback function that was be fired when the event occured + */ + this.removeEventListener = this.off = function ( type, listener ) { + + var index = listeners[ type ].indexOf( listener ); + + if ( index !== - 1 ) { + + listeners[ type ].splice( index, 1 ); + + } + + }; + + /** + * Removes all the listeners that were active for the specified event type + * + * @method removeAllEventListeners + * @param type {string} A string representing the event type which will have all its listeners removed + */ + this.removeAllEventListeners = function( type ) { + var a = listeners[type]; + if (a) + a.length = 0; + }; +}; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +/** + * This helper function will automatically detect which renderer you should be using. + * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by + * the browser then this function will return a canvas renderer + * @class autoDetectRenderer + * @static + * @param width=800 {Number} the width of the renderers view + * @param height=600 {Number} the height of the renderers view + * @param [view] {Canvas} the canvas to use as a view, optional + * @param [transparent=false] {Boolean} the transparency of the render view, default false + * @param [antialias=false] {Boolean} sets antialias (only applicable in webGL chrome at the moment) + * + */ +PIXI.autoDetectRenderer = function(width, height, view, transparent, antialias) +{ + if(!width)width = 800; + if(!height)height = 600; + + // BORROWED from Mr Doob (mrdoob.com) + var webgl = ( function () { try { + var canvas = document.createElement( 'canvas' ); + return !! window.WebGLRenderingContext && ( canvas.getContext( 'webgl' ) || canvas.getContext( 'experimental-webgl' ) ); + } catch( e ) { + return false; + } + } )(); + + if( webgl ) + { + return new PIXI.WebGLRenderer(width, height, view, transparent, antialias); + } + + return new PIXI.CanvasRenderer(width, height, view, transparent); +}; + +/** + * This helper function will automatically detect which renderer you should be using. + * This function is very similar to the autoDetectRenderer function except that is will return a canvas renderer for android. + * Even thought both android chrome suports webGL the canvas implementation perform better at the time of writing. + * This function will likely change and update as webGL performance imporoves on thease devices. + * @class getRecommendedRenderer + * @static + * @param width=800 {Number} the width of the renderers view + * @param height=600 {Number} the height of the renderers view + * @param [view] {Canvas} the canvas to use as a view, optional + * @param [transparent=false] {Boolean} the transparency of the render view, default false + * @param [antialias=false] {Boolean} sets antialias (only applicable in webGL chrome at the moment) + * + */ +PIXI.autoDetectRecommendedRenderer = function(width, height, view, transparent, antialias) +{ + if(!width)width = 800; + if(!height)height = 600; + + // BORROWED from Mr Doob (mrdoob.com) + var webgl = ( function () { try { + var canvas = document.createElement( 'canvas' ); + return !! window.WebGLRenderingContext && ( canvas.getContext( 'webgl' ) || canvas.getContext( 'experimental-webgl' ) ); + } catch( e ) { + return false; + } + } )(); + + var isAndroid = /Android/i.test(navigator.userAgent); + + if( webgl && !isAndroid) + { + return new PIXI.WebGLRenderer(width, height, view, transparent, antialias); + } + + return new PIXI.CanvasRenderer(width, height, view, transparent); +}; + +/* + PolyK library + url: http://polyk.ivank.net + Released under MIT licence. + + Copyright (c) 2012 Ivan Kuckir + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + + This is an amazing lib! + + slightly modified by Mat Groves (matgroves.com); +*/ + +/** + * Based on the Polyk library http://polyk.ivank.net released under MIT licence. + * This is an amazing lib! + * slightly modified by Mat Groves (matgroves.com); + * @class PolyK + * + */ +PIXI.PolyK = {}; + +/** + * Triangulates shapes for webGL graphic fills + * + * @method Triangulate + * + */ +PIXI.PolyK.Triangulate = function(p) +{ + var sign = true; + + var n = p.length >> 1; + if(n < 3) return []; + + var tgs = []; + var avl = []; + for(var i = 0; i < n; i++) avl.push(i); + + i = 0; + var al = n; + while(al > 3) + { + var i0 = avl[(i+0)%al]; + var i1 = avl[(i+1)%al]; + var i2 = avl[(i+2)%al]; + + var ax = p[2*i0], ay = p[2*i0+1]; + var bx = p[2*i1], by = p[2*i1+1]; + var cx = p[2*i2], cy = p[2*i2+1]; + + var earFound = false; + if(PIXI.PolyK._convex(ax, ay, bx, by, cx, cy, sign)) + { + earFound = true; + for(var j = 0; j < al; j++) + { + var vi = avl[j]; + if(vi === i0 || vi === i1 || vi === i2) continue; + + if(PIXI.PolyK._PointInTriangle(p[2*vi], p[2*vi+1], ax, ay, bx, by, cx, cy)) { + earFound = false; + break; + } + } + } + + if(earFound) + { + tgs.push(i0, i1, i2); + avl.splice((i+1)%al, 1); + al--; + i = 0; + } + else if(i++ > 3*al) + { + // need to flip flip reverse it! + // reset! + if(sign) + { + tgs = []; + avl = []; + for(i = 0; i < n; i++) avl.push(i); + + i = 0; + al = n; + + sign = false; + } + else + { + window.console.log("PIXI Warning: shape too complex to fill"); + return []; + } + } + } + + tgs.push(avl[0], avl[1], avl[2]); + return tgs; +}; + +/** + * Checks whether a point is within a triangle + * + * @method _PointInTriangle + * @param px {Number} x coordinate of the point to test + * @param py {Number} y coordinate of the point to test + * @param ax {Number} x coordinate of the a point of the triangle + * @param ay {Number} y coordinate of the a point of the triangle + * @param bx {Number} x coordinate of the b point of the triangle + * @param by {Number} y coordinate of the b point of the triangle + * @param cx {Number} x coordinate of the c point of the triangle + * @param cy {Number} y coordinate of the c point of the triangle + * @private + */ +PIXI.PolyK._PointInTriangle = function(px, py, ax, ay, bx, by, cx, cy) +{ + var v0x = cx-ax; + var v0y = cy-ay; + var v1x = bx-ax; + var v1y = by-ay; + var v2x = px-ax; + var v2y = py-ay; + + var dot00 = v0x*v0x+v0y*v0y; + var dot01 = v0x*v1x+v0y*v1y; + var dot02 = v0x*v2x+v0y*v2y; + var dot11 = v1x*v1x+v1y*v1y; + var dot12 = v1x*v2x+v1y*v2y; + + var invDenom = 1 / (dot00 * dot11 - dot01 * dot01); + var u = (dot11 * dot02 - dot01 * dot12) * invDenom; + var v = (dot00 * dot12 - dot01 * dot02) * invDenom; + + // Check if point is in triangle + return (u >= 0) && (v >= 0) && (u + v < 1); +}; + +/** + * Checks whether a shape is convex + * + * @method _convex + * + * @private + */ +PIXI.PolyK._convex = function(ax, ay, bx, by, cx, cy, sign) +{ + return ((ay-by)*(cx-bx) + (bx-ax)*(cy-by) >= 0) === sign; +}; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +// TODO Alvin and Mat +// Should we eventually create a Utils class ? +// Or just move this file to the pixi.js file ? +PIXI.initDefaultShaders = function() +{ + + // PIXI.stripShader = new PIXI.StripShader(); +// PIXI.stripShader.init(); + +}; + +PIXI.CompileVertexShader = function(gl, shaderSrc) +{ + return PIXI._CompileShader(gl, shaderSrc, gl.VERTEX_SHADER); +}; + +PIXI.CompileFragmentShader = function(gl, shaderSrc) +{ + return PIXI._CompileShader(gl, shaderSrc, gl.FRAGMENT_SHADER); +}; + +PIXI._CompileShader = function(gl, shaderSrc, shaderType) +{ + var src = shaderSrc.join("\n"); + var shader = gl.createShader(shaderType); + gl.shaderSource(shader, src); + gl.compileShader(shader); + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + window.console.log(gl.getShaderInfoLog(shader)); + return null; + } + + return shader; +}; + +PIXI.compileProgram = function(gl, vertexSrc, fragmentSrc) +{ + var fragmentShader = PIXI.CompileFragmentShader(gl, fragmentSrc); + var vertexShader = PIXI.CompileVertexShader(gl, vertexSrc); + + var shaderProgram = gl.createProgram(); + + gl.attachShader(shaderProgram, vertexShader); + gl.attachShader(shaderProgram, fragmentShader); + gl.linkProgram(shaderProgram); + + if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { + window.console.log("Could not initialise shaders"); + } + + return shaderProgram; +}; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + * @author Richard Davey http://www.photonstorm.com @photonstorm + */ + +/** +* @class PixiShader +* @constructor +*/ +PIXI.PixiShader = function(gl) +{ + /** + * @property gl + * @type WebGLContext + */ + this.gl = gl; + + /** + * @property {any} program - The WebGL program. + */ + this.program = null; + + /** + * @property {array} fragmentSrc - The fragment shader. + */ + this.fragmentSrc = [ + 'precision lowp float;', + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'uniform sampler2D uSampler;', + 'void main(void) {', + ' gl_FragColor = texture2D(uSampler, vTextureCoord) * vColor ;', + '}' + ]; + + /** + * @property {number} textureCount - A local texture counter for multi-texture shaders. + */ + this.textureCount = 0; + + this.attributes = []; + + this.init(); +}; + +/** +* Initialises the shader +* @method init +* +*/ +PIXI.PixiShader.prototype.init = function() +{ + var gl = this.gl; + + var program = PIXI.compileProgram(gl, this.vertexSrc || PIXI.PixiShader.defaultVertexSrc, this.fragmentSrc); + + gl.useProgram(program); + + // get and store the uniforms for the shader + this.uSampler = gl.getUniformLocation(program, 'uSampler'); + this.projectionVector = gl.getUniformLocation(program, 'projectionVector'); + this.offsetVector = gl.getUniformLocation(program, 'offsetVector'); + this.dimensions = gl.getUniformLocation(program, 'dimensions'); + + // get and store the attributes + this.aVertexPosition = gl.getAttribLocation(program, 'aVertexPosition'); + this.aTextureCoord = gl.getAttribLocation(program, 'aTextureCoord'); + this.colorAttribute = gl.getAttribLocation(program, 'aColor'); + + + // Begin worst hack eva // + + // WHY??? ONLY on my chrome pixel the line above returns -1 when using filters? + // maybe its something to do with the current state of the gl context. + // Im convinced this is a bug in the chrome browser as there is NO reason why this should be returning -1 especially as it only manifests on my chrome pixel + // If theres any webGL people that know why could happen please help :) + if(this.colorAttribute === -1) + { + this.colorAttribute = 2; + } + + this.attributes = [this.aVertexPosition, this.aTextureCoord, this.colorAttribute]; + + // End worst hack eva // + + // add those custom shaders! + for (var key in this.uniforms) + { + // get the uniform locations.. + this.uniforms[key].uniformLocation = gl.getUniformLocation(program, key); + } + + this.initUniforms(); + + this.program = program; +}; + +/** +* Initialises the shader uniform values. +* Uniforms are specified in the GLSL_ES Specification: http://www.khronos.org/registry/webgl/specs/latest/1.0/ +* http://www.khronos.org/registry/gles/specs/2.0/GLSL_ES_Specification_1.0.17.pdf +* +* @method initUniforms +*/ +PIXI.PixiShader.prototype.initUniforms = function() +{ + this.textureCount = 1; + var gl = this.gl; + var uniform; + + for (var key in this.uniforms) + { + uniform = this.uniforms[key]; + + var type = uniform.type; + + if (type === 'sampler2D') + { + uniform._init = false; + + if (uniform.value !== null) + { + this.initSampler2D(uniform); + } + } + else if (type === 'mat2' || type === 'mat3' || type === 'mat4') + { + // These require special handling + uniform.glMatrix = true; + uniform.glValueLength = 1; + + if (type === 'mat2') + { + uniform.glFunc = gl.uniformMatrix2fv; + } + else if (type === 'mat3') + { + uniform.glFunc = gl.uniformMatrix3fv; + } + else if (type === 'mat4') + { + uniform.glFunc = gl.uniformMatrix4fv; + } + } + else + { + // GL function reference + uniform.glFunc = gl['uniform' + type]; + + if (type === '2f' || type === '2i') + { + uniform.glValueLength = 2; + } + else if (type === '3f' || type === '3i') + { + uniform.glValueLength = 3; + } + else if (type === '4f' || type === '4i') + { + uniform.glValueLength = 4; + } + else + { + uniform.glValueLength = 1; + } + } + } + +}; + +/** +* Initialises a Sampler2D uniform (which may only be available later on after initUniforms once the texture has loaded) +* +* @method initSampler2D +*/ +PIXI.PixiShader.prototype.initSampler2D = function(uniform) +{ + if (!uniform.value || !uniform.value.baseTexture || !uniform.value.baseTexture.hasLoaded) + { + return; + } + + var gl = this.gl; + + gl.activeTexture(gl['TEXTURE' + this.textureCount]); + gl.bindTexture(gl.TEXTURE_2D, uniform.value.baseTexture._glTextures[gl.id]); + + // Extended texture data + if (uniform.textureData) + { + var data = uniform.textureData; + + // GLTexture = mag linear, min linear_mipmap_linear, wrap repeat + gl.generateMipmap(gl.TEXTURE_2D); + // GLTextureLinear = mag/min linear, wrap clamp + // GLTextureNearestRepeat = mag/min NEAREST, wrap repeat + // GLTextureNearest = mag/min nearest, wrap clamp + // AudioTexture = whatever + luminance + width 512, height 2, border 0 + // KeyTexture = whatever + luminance + width 256, height 2, border 0 + + // magFilter can be: gl.LINEAR, gl.LINEAR_MIPMAP_LINEAR or gl.NEAREST + // wrapS/T can be: gl.CLAMP_TO_EDGE or gl.REPEAT + + var magFilter = (data.magFilter) ? data.magFilter : gl.LINEAR; + var minFilter = (data.minFilter) ? data.minFilter : gl.LINEAR; + var wrapS = (data.wrapS) ? data.wrapS : gl.CLAMP_TO_EDGE; + var wrapT = (data.wrapT) ? data.wrapT : gl.CLAMP_TO_EDGE; + var format = (data.luminance) ? gl.LUMINANCE : gl.RGBA; + + if (data.repeat) + { + wrapS = gl.REPEAT; + wrapT = gl.REPEAT; + } + + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !!data.flipY); + + if (data.width) + { + var width = (data.width) ? data.width : 512; + var height = (data.height) ? data.height : 2; + var border = (data.border) ? data.border : 0; + + // void texImage2D(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, ArrayBufferView? pixels); + gl.texImage2D(gl.TEXTURE_2D, 0, format, width, height, border, format, gl.UNSIGNED_BYTE, null); + } + else + { + // void texImage2D(GLenum target, GLint level, GLenum internalformat, GLenum format, GLenum type, ImageData? pixels); + gl.texImage2D(gl.TEXTURE_2D, 0, format, gl.RGBA, gl.UNSIGNED_BYTE, uniform.value.baseTexture.source); + } + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, magFilter); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minFilter); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapS); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapT); + } + + gl.uniform1i(uniform.uniformLocation, this.textureCount); + + uniform._init = true; + + this.textureCount++; + +}; + +/** +* Updates the shader uniform values. +* +* @method syncUniforms +*/ +PIXI.PixiShader.prototype.syncUniforms = function() +{ + this.textureCount = 1; + var uniform; + var gl = this.gl; + + // This would probably be faster in an array and it would guarantee key order + for (var key in this.uniforms) + { + uniform = this.uniforms[key]; + + if (uniform.glValueLength === 1) + { + if (uniform.glMatrix === true) + { + uniform.glFunc.call(gl, uniform.uniformLocation, uniform.transpose, uniform.value); + } + else + { + uniform.glFunc.call(gl, uniform.uniformLocation, uniform.value); + } + } + else if (uniform.glValueLength === 2) + { + uniform.glFunc.call(gl, uniform.uniformLocation, uniform.value.x, uniform.value.y); + } + else if (uniform.glValueLength === 3) + { + uniform.glFunc.call(gl, uniform.uniformLocation, uniform.value.x, uniform.value.y, uniform.value.z); + } + else if (uniform.glValueLength === 4) + { + uniform.glFunc.call(gl, uniform.uniformLocation, uniform.value.x, uniform.value.y, uniform.value.z, uniform.value.w); + } + else if (uniform.type === 'sampler2D') + { + if (uniform._init) + { + gl.activeTexture(gl['TEXTURE' + this.textureCount]); + gl.bindTexture(gl.TEXTURE_2D, uniform.value.baseTexture._glTextures[gl.id] || PIXI.createWebGLTexture( uniform.value.baseTexture, gl)); + gl.uniform1i(uniform.uniformLocation, this.textureCount); + this.textureCount++; + } + else + { + this.initSampler2D(uniform); + } + } + } + +}; + +/** +* Destroys the shader +* @method destroy +*/ +PIXI.PixiShader.prototype.destroy = function() +{ + this.gl.deleteProgram( this.program ); + this.uniforms = null; + this.gl = null; + + this.attributes = null; +}; + +/** +* The Default Vertex shader source +* @property defaultVertexSrc +* @type String +*/ +PIXI.PixiShader.defaultVertexSrc = [ + 'attribute vec2 aVertexPosition;', + 'attribute vec2 aTextureCoord;', + 'attribute vec2 aColor;', + + 'uniform vec2 projectionVector;', + 'uniform vec2 offsetVector;', + + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + + 'const vec2 center = vec2(-1.0, 1.0);', + + 'void main(void) {', + ' gl_Position = vec4( ((aVertexPosition + offsetVector) / projectionVector) + center , 0.0, 1.0);', + ' vTextureCoord = aTextureCoord;', + ' vec3 color = mod(vec3(aColor.y/65536.0, aColor.y/256.0, aColor.y), 256.0) / 256.0;', + ' vColor = vec4(color * aColor.x, aColor.x);', + '}' +]; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + * @author Richard Davey http://www.photonstorm.com @photonstorm + */ + +/** +* @class PixiFastShader +* @constructor +* @param gl {WebGLContext} the current WebGL drawing context +*/ +PIXI.PixiFastShader = function(gl) +{ + + /** + * @property gl + * @type WebGLContext + */ + this.gl = gl; + + /** + * @property {any} program - The WebGL program. + */ + this.program = null; + + /** + * @property {array} fragmentSrc - The fragment shader. + */ + this.fragmentSrc = [ + 'precision lowp float;', + 'varying vec2 vTextureCoord;', + 'varying float vColor;', + 'uniform sampler2D uSampler;', + 'void main(void) {', + ' gl_FragColor = texture2D(uSampler, vTextureCoord) * vColor ;', + '}' + ]; + + /** + * @property {array} vertexSrc - The vertex shader + */ + this.vertexSrc = [ + 'attribute vec2 aVertexPosition;', + 'attribute vec2 aPositionCoord;', + 'attribute vec2 aScale;', + 'attribute float aRotation;', + 'attribute vec2 aTextureCoord;', + 'attribute float aColor;', + + 'uniform vec2 projectionVector;', + 'uniform vec2 offsetVector;', + 'uniform mat3 uMatrix;', + + 'varying vec2 vTextureCoord;', + 'varying float vColor;', + + 'const vec2 center = vec2(-1.0, 1.0);', + + 'void main(void) {', + ' vec2 v;', + ' vec2 sv = aVertexPosition * aScale;', + ' v.x = (sv.x) * cos(aRotation) - (sv.y) * sin(aRotation);', + ' v.y = (sv.x) * sin(aRotation) + (sv.y) * cos(aRotation);', + ' v = ( uMatrix * vec3(v + aPositionCoord , 1.0) ).xy ;', + ' gl_Position = vec4( ( v / projectionVector) + center , 0.0, 1.0);', + ' vTextureCoord = aTextureCoord;', + // ' vec3 color = mod(vec3(aColor.y/65536.0, aColor.y/256.0, aColor.y), 256.0) / 256.0;', + ' vColor = aColor;', + '}' + ]; + + + /** + * @property {number} textureCount - A local texture counter for multi-texture shaders. + */ + this.textureCount = 0; + + + this.init(); +}; + +/** +* Initialises the shader +* @method init +* +*/ +PIXI.PixiFastShader.prototype.init = function() +{ + + var gl = this.gl; + + var program = PIXI.compileProgram(gl, this.vertexSrc, this.fragmentSrc); + + gl.useProgram(program); + + // get and store the uniforms for the shader + this.uSampler = gl.getUniformLocation(program, 'uSampler'); + + this.projectionVector = gl.getUniformLocation(program, 'projectionVector'); + this.offsetVector = gl.getUniformLocation(program, 'offsetVector'); + this.dimensions = gl.getUniformLocation(program, 'dimensions'); + this.uMatrix = gl.getUniformLocation(program, 'uMatrix'); + + // get and store the attributes + this.aVertexPosition = gl.getAttribLocation(program, 'aVertexPosition'); + this.aPositionCoord = gl.getAttribLocation(program, 'aPositionCoord'); + + this.aScale = gl.getAttribLocation(program, 'aScale'); + this.aRotation = gl.getAttribLocation(program, 'aRotation'); + + this.aTextureCoord = gl.getAttribLocation(program, 'aTextureCoord'); + this.colorAttribute = gl.getAttribLocation(program, 'aColor'); + + + + // Begin worst hack eva // + + // WHY??? ONLY on my chrome pixel the line above returns -1 when using filters? + // maybe its somthing to do with the current state of the gl context. + // Im convinced this is a bug in the chrome browser as there is NO reason why this should be returning -1 especially as it only manifests on my chrome pixel + // If theres any webGL people that know why could happen please help :) + if(this.colorAttribute === -1) + { + this.colorAttribute = 2; + } + + this.attributes = [this.aVertexPosition, this.aPositionCoord, this.aScale, this.aRotation, this.aTextureCoord, this.colorAttribute]; + + // End worst hack eva // + + + this.program = program; +}; + +/** +* Destroys the shader +* @method destroy +* +*/ +PIXI.PixiFastShader.prototype.destroy = function() +{ + this.gl.deleteProgram( this.program ); + this.uniforms = null; + this.gl = null; + + this.attributes = null; +}; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + + +PIXI.StripShader = function() +{ + /** + * @property {any} program - The WebGL program. + */ + this.program = null; + + /** + * @property {array} fragmentSrc - The fragment shader. + */ + this.fragmentSrc = [ + 'precision mediump float;', + 'varying vec2 vTextureCoord;', + 'varying float vColor;', + 'uniform float alpha;', + 'uniform sampler2D uSampler;', + + 'void main(void) {', + ' gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.x, vTextureCoord.y));', + ' gl_FragColor = gl_FragColor * alpha;', + '}' + ]; + + /** + * @property {array} fragmentSrc - The fragment shader. + */ + this.vertexSrc = [ + 'attribute vec2 aVertexPosition;', + 'attribute vec2 aTextureCoord;', + 'attribute float aColor;', + 'uniform mat3 translationMatrix;', + 'uniform vec2 projectionVector;', + 'varying vec2 vTextureCoord;', + 'uniform vec2 offsetVector;', + 'varying float vColor;', + + 'void main(void) {', + ' vec3 v = translationMatrix * vec3(aVertexPosition, 1.0);', + ' v -= offsetVector.xyx;', + ' gl_Position = vec4( v.x / projectionVector.x -1.0, v.y / projectionVector.y + 1.0 , 0.0, 1.0);', + ' vTextureCoord = aTextureCoord;', + ' vColor = aColor;', + '}' + ]; +}; + +/** +* Initialises the shader +* @method init +* +*/ +PIXI.StripShader.prototype.init = function() +{ + + var gl = PIXI.gl; + + var program = PIXI.compileProgram(gl, this.vertexSrc, this.fragmentSrc); + gl.useProgram(program); + + // get and store the uniforms for the shader + this.uSampler = gl.getUniformLocation(program, 'uSampler'); + this.projectionVector = gl.getUniformLocation(program, 'projectionVector'); + this.offsetVector = gl.getUniformLocation(program, 'offsetVector'); + this.colorAttribute = gl.getAttribLocation(program, 'aColor'); + //this.dimensions = gl.getUniformLocation(this.program, 'dimensions'); + + // get and store the attributes + this.aVertexPosition = gl.getAttribLocation(program, 'aVertexPosition'); + this.aTextureCoord = gl.getAttribLocation(program, 'aTextureCoord'); + + this.translationMatrix = gl.getUniformLocation(program, 'translationMatrix'); + this.alpha = gl.getUniformLocation(program, 'alpha'); + + this.program = program; +}; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +/** +* @class PrimitiveShader +* @constructor +* @param gl {WebGLContext} the current WebGL drawing context +*/ +PIXI.PrimitiveShader = function(gl) +{ + /** + * @property gl + * @type WebGLContext + */ + this.gl = gl; + + /** + * @property {any} program - The WebGL program. + */ + this.program = null; + + /** + * @property fragmentSrc + * @type Array + */ + this.fragmentSrc = [ + 'precision mediump float;', + 'varying vec4 vColor;', + + 'void main(void) {', + ' gl_FragColor = vColor;', + '}' + ]; + + /** + * @property vertexSrc + * @type Array + */ + this.vertexSrc = [ + 'attribute vec2 aVertexPosition;', + 'attribute vec4 aColor;', + 'uniform mat3 translationMatrix;', + 'uniform vec2 projectionVector;', + 'uniform vec2 offsetVector;', + 'uniform float alpha;', + 'uniform vec3 tint;', + 'varying vec4 vColor;', + + 'void main(void) {', + ' vec3 v = translationMatrix * vec3(aVertexPosition , 1.0);', + ' v -= offsetVector.xyx;', + ' gl_Position = vec4( v.x / projectionVector.x -1.0, v.y / -projectionVector.y + 1.0 , 0.0, 1.0);', + ' vColor = aColor * vec4(tint * alpha, alpha);', + '}' + ]; + + this.init(); +}; + +/** +* Initialises the shader +* @method init +* +*/ +PIXI.PrimitiveShader.prototype.init = function() +{ + + var gl = this.gl; + + var program = PIXI.compileProgram(gl, this.vertexSrc, this.fragmentSrc); + gl.useProgram(program); + + // get and store the uniforms for the shader + this.projectionVector = gl.getUniformLocation(program, 'projectionVector'); + this.offsetVector = gl.getUniformLocation(program, 'offsetVector'); + this.tintColor = gl.getUniformLocation(program, 'tint'); + + + // get and store the attributes + this.aVertexPosition = gl.getAttribLocation(program, 'aVertexPosition'); + this.colorAttribute = gl.getAttribLocation(program, 'aColor'); + + this.attributes = [this.aVertexPosition, this.colorAttribute]; + + this.translationMatrix = gl.getUniformLocation(program, 'translationMatrix'); + this.alpha = gl.getUniformLocation(program, 'alpha'); + + this.program = program; +}; + +/** +* Destroys the shader +* @method destroy +* +*/ +PIXI.PrimitiveShader.prototype.destroy = function() +{ + this.gl.deleteProgram( this.program ); + this.uniforms = null; + this.gl = null; + + this.attribute = null; +}; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +/** + * A set of functions used by the webGL renderer to draw the primitive graphics data + * + * @class WebGLGraphics + * @private + * @static + */ +PIXI.WebGLGraphics = function() +{ + +}; + +/** + * Renders the graphics object + * + * @static + * @private + * @method renderGraphics + * @param graphics {Graphics} + * @param renderSession {Object} + */ +PIXI.WebGLGraphics.renderGraphics = function(graphics, renderSession)//projection, offset) +{ + var gl = renderSession.gl; + var projection = renderSession.projection, + offset = renderSession.offset, + shader = renderSession.shaderManager.primitiveShader; + + if(!graphics._webGL[gl.id])graphics._webGL[gl.id] = {points:[], indices:[], lastIndex:0, + buffer:gl.createBuffer(), + indexBuffer:gl.createBuffer()}; + + var webGL = graphics._webGL[gl.id]; + + if(graphics.dirty) + { + graphics.dirty = false; + + if(graphics.clearDirty) + { + graphics.clearDirty = false; + + webGL.lastIndex = 0; + webGL.points = []; + webGL.indices = []; + + } + + PIXI.WebGLGraphics.updateGraphics(graphics, gl); + } + + renderSession.shaderManager.activatePrimitiveShader(); + + // This could be speeded up for sure! + + // set the matrix transform + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + + gl.uniformMatrix3fv(shader.translationMatrix, false, graphics.worldTransform.toArray(true)); + + gl.uniform2f(shader.projectionVector, projection.x, -projection.y); + gl.uniform2f(shader.offsetVector, -offset.x, -offset.y); + + gl.uniform3fv(shader.tintColor, PIXI.hex2rgb(graphics.tint)); + + gl.uniform1f(shader.alpha, graphics.worldAlpha); + gl.bindBuffer(gl.ARRAY_BUFFER, webGL.buffer); + + gl.vertexAttribPointer(shader.aVertexPosition, 2, gl.FLOAT, false, 4 * 6, 0); + gl.vertexAttribPointer(shader.colorAttribute, 4, gl.FLOAT, false,4 * 6, 2 * 4); + + // set the index buffer! + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, webGL.indexBuffer); + + gl.drawElements(gl.TRIANGLE_STRIP, webGL.indices.length, gl.UNSIGNED_SHORT, 0 ); + + renderSession.shaderManager.deactivatePrimitiveShader(); + + // return to default shader... +// PIXI.activateShader(PIXI.defaultShader); +}; + +/** + * Updates the graphics object + * + * @static + * @private + * @method updateGraphics + * @param graphicsData {Graphics} The graphics object to update + * @param gl {WebGLContext} the current WebGL drawing context + */ +PIXI.WebGLGraphics.updateGraphics = function(graphics, gl) +{ + var webGL = graphics._webGL[gl.id]; + + for (var i = webGL.lastIndex; i < graphics.graphicsData.length; i++) + { + var data = graphics.graphicsData[i]; + + if(data.type === PIXI.Graphics.POLY) + { + if(data.fill) + { + if(data.points.length>3) + PIXI.WebGLGraphics.buildPoly(data, webGL); + } + + if(data.lineWidth > 0) + { + PIXI.WebGLGraphics.buildLine(data, webGL); + } + } + else if(data.type === PIXI.Graphics.RECT) + { + PIXI.WebGLGraphics.buildRectangle(data, webGL); + } + else if(data.type === PIXI.Graphics.CIRC || data.type === PIXI.Graphics.ELIP) + { + PIXI.WebGLGraphics.buildCircle(data, webGL); + } + } + + webGL.lastIndex = graphics.graphicsData.length; + + + + webGL.glPoints = new Float32Array(webGL.points); + + gl.bindBuffer(gl.ARRAY_BUFFER, webGL.buffer); + gl.bufferData(gl.ARRAY_BUFFER, webGL.glPoints, gl.STATIC_DRAW); + + webGL.glIndicies = new Uint16Array(webGL.indices); + + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, webGL.indexBuffer); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, webGL.glIndicies, gl.STATIC_DRAW); +}; + +/** + * Builds a rectangle to draw + * + * @static + * @private + * @method buildRectangle + * @param graphicsData {Graphics} The graphics object containing all the necessary properties + * @param webGLData {Object} + */ +PIXI.WebGLGraphics.buildRectangle = function(graphicsData, webGLData) +{ + // --- // + // need to convert points to a nice regular data + // + var rectData = graphicsData.points; + var x = rectData[0]; + var y = rectData[1]; + var width = rectData[2]; + var height = rectData[3]; + + + if(graphicsData.fill) + { + var color = PIXI.hex2rgb(graphicsData.fillColor); + var alpha = graphicsData.fillAlpha; + + var r = color[0] * alpha; + var g = color[1] * alpha; + var b = color[2] * alpha; + + var verts = webGLData.points; + var indices = webGLData.indices; + + var vertPos = verts.length/6; + + // start + verts.push(x, y); + verts.push(r, g, b, alpha); + + verts.push(x + width, y); + verts.push(r, g, b, alpha); + + verts.push(x , y + height); + verts.push(r, g, b, alpha); + + verts.push(x + width, y + height); + verts.push(r, g, b, alpha); + + // insert 2 dead triangles.. + indices.push(vertPos, vertPos, vertPos+1, vertPos+2, vertPos+3, vertPos+3); + } + + if(graphicsData.lineWidth) + { + var tempPoints = graphicsData.points; + + graphicsData.points = [x, y, + x + width, y, + x + width, y + height, + x, y + height, + x, y]; + + + PIXI.WebGLGraphics.buildLine(graphicsData, webGLData); + + graphicsData.points = tempPoints; + } +}; + +/** + * Builds a circle to draw + * + * @static + * @private + * @method buildCircle + * @param graphicsData {Graphics} The graphics object to draw + * @param webGLData {Object} + */ +PIXI.WebGLGraphics.buildCircle = function(graphicsData, webGLData) +{ + + // need to convert points to a nice regular data + var rectData = graphicsData.points; + var x = rectData[0]; + var y = rectData[1]; + var width = rectData[2]; + var height = rectData[3]; + + var totalSegs = 40; + var seg = (Math.PI * 2) / totalSegs ; + + var i = 0; + + if(graphicsData.fill) + { + var color = PIXI.hex2rgb(graphicsData.fillColor); + var alpha = graphicsData.fillAlpha; + + var r = color[0] * alpha; + var g = color[1] * alpha; + var b = color[2] * alpha; + + var verts = webGLData.points; + var indices = webGLData.indices; + + var vecPos = verts.length/6; + + indices.push(vecPos); + + for (i = 0; i < totalSegs + 1 ; i++) + { + verts.push(x,y, r, g, b, alpha); + + verts.push(x + Math.sin(seg * i) * width, + y + Math.cos(seg * i) * height, + r, g, b, alpha); + + indices.push(vecPos++, vecPos++); + } + + indices.push(vecPos-1); + } + + if(graphicsData.lineWidth) + { + var tempPoints = graphicsData.points; + + graphicsData.points = []; + + for (i = 0; i < totalSegs + 1; i++) + { + graphicsData.points.push(x + Math.sin(seg * i) * width, + y + Math.cos(seg * i) * height); + } + + PIXI.WebGLGraphics.buildLine(graphicsData, webGLData); + + graphicsData.points = tempPoints; + } +}; + +/** + * Builds a line to draw + * + * @static + * @private + * @method buildLine + * @param graphicsData {Graphics} The graphics object containing all the necessary properties + * @param webGLData {Object} + */ +PIXI.WebGLGraphics.buildLine = function(graphicsData, webGLData) +{ + // TODO OPTIMISE! + var i = 0; + + var points = graphicsData.points; + if(points.length === 0)return; + + // if the line width is an odd number add 0.5 to align to a whole pixel + if(graphicsData.lineWidth%2) + { + for (i = 0; i < points.length; i++) { + points[i] += 0.5; + } + } + + // get first and last point.. figure out the middle! + var firstPoint = new PIXI.Point( points[0], points[1] ); + var lastPoint = new PIXI.Point( points[points.length - 2], points[points.length - 1] ); + + // if the first point is the last point - gonna have issues :) + if(firstPoint.x === lastPoint.x && firstPoint.y === lastPoint.y) + { + points.pop(); + points.pop(); + + lastPoint = new PIXI.Point( points[points.length - 2], points[points.length - 1] ); + + var midPointX = lastPoint.x + (firstPoint.x - lastPoint.x) *0.5; + var midPointY = lastPoint.y + (firstPoint.y - lastPoint.y) *0.5; + + points.unshift(midPointX, midPointY); + points.push(midPointX, midPointY); + } + + var verts = webGLData.points; + var indices = webGLData.indices; + var length = points.length / 2; + var indexCount = points.length; + var indexStart = verts.length/6; + + // DRAW the Line + var width = graphicsData.lineWidth / 2; + + // sort color + var color = PIXI.hex2rgb(graphicsData.lineColor); + var alpha = graphicsData.lineAlpha; + var r = color[0] * alpha; + var g = color[1] * alpha; + var b = color[2] * alpha; + + var px, py, p1x, p1y, p2x, p2y, p3x, p3y; + var perpx, perpy, perp2x, perp2y, perp3x, perp3y; + var a1, b1, c1, a2, b2, c2; + var denom, pdist, dist; + + p1x = points[0]; + p1y = points[1]; + + p2x = points[2]; + p2y = points[3]; + + perpx = -(p1y - p2y); + perpy = p1x - p2x; + + dist = Math.sqrt(perpx*perpx + perpy*perpy); + + perpx /= dist; + perpy /= dist; + perpx *= width; + perpy *= width; + + // start + verts.push(p1x - perpx , p1y - perpy, + r, g, b, alpha); + + verts.push(p1x + perpx , p1y + perpy, + r, g, b, alpha); + + for (i = 1; i < length-1; i++) + { + p1x = points[(i-1)*2]; + p1y = points[(i-1)*2 + 1]; + + p2x = points[(i)*2]; + p2y = points[(i)*2 + 1]; + + p3x = points[(i+1)*2]; + p3y = points[(i+1)*2 + 1]; + + perpx = -(p1y - p2y); + perpy = p1x - p2x; + + dist = Math.sqrt(perpx*perpx + perpy*perpy); + perpx /= dist; + perpy /= dist; + perpx *= width; + perpy *= width; + + perp2x = -(p2y - p3y); + perp2y = p2x - p3x; + + dist = Math.sqrt(perp2x*perp2x + perp2y*perp2y); + perp2x /= dist; + perp2y /= dist; + perp2x *= width; + perp2y *= width; + + a1 = (-perpy + p1y) - (-perpy + p2y); + b1 = (-perpx + p2x) - (-perpx + p1x); + c1 = (-perpx + p1x) * (-perpy + p2y) - (-perpx + p2x) * (-perpy + p1y); + a2 = (-perp2y + p3y) - (-perp2y + p2y); + b2 = (-perp2x + p2x) - (-perp2x + p3x); + c2 = (-perp2x + p3x) * (-perp2y + p2y) - (-perp2x + p2x) * (-perp2y + p3y); + + denom = a1*b2 - a2*b1; + + if(Math.abs(denom) < 0.1 ) + { + + denom+=10.1; + verts.push(p2x - perpx , p2y - perpy, + r, g, b, alpha); + + verts.push(p2x + perpx , p2y + perpy, + r, g, b, alpha); + + continue; + } + + px = (b1*c2 - b2*c1)/denom; + py = (a2*c1 - a1*c2)/denom; + + + pdist = (px -p2x) * (px -p2x) + (py -p2y) + (py -p2y); + + + if(pdist > 140 * 140) + { + perp3x = perpx - perp2x; + perp3y = perpy - perp2y; + + dist = Math.sqrt(perp3x*perp3x + perp3y*perp3y); + perp3x /= dist; + perp3y /= dist; + perp3x *= width; + perp3y *= width; + + verts.push(p2x - perp3x, p2y -perp3y); + verts.push(r, g, b, alpha); + + verts.push(p2x + perp3x, p2y +perp3y); + verts.push(r, g, b, alpha); + + verts.push(p2x - perp3x, p2y -perp3y); + verts.push(r, g, b, alpha); + + indexCount++; + } + else + { + + verts.push(px , py); + verts.push(r, g, b, alpha); + + verts.push(p2x - (px-p2x), p2y - (py - p2y)); + verts.push(r, g, b, alpha); + } + } + + p1x = points[(length-2)*2]; + p1y = points[(length-2)*2 + 1]; + + p2x = points[(length-1)*2]; + p2y = points[(length-1)*2 + 1]; + + perpx = -(p1y - p2y); + perpy = p1x - p2x; + + dist = Math.sqrt(perpx*perpx + perpy*perpy); + perpx /= dist; + perpy /= dist; + perpx *= width; + perpy *= width; + + verts.push(p2x - perpx , p2y - perpy); + verts.push(r, g, b, alpha); + + verts.push(p2x + perpx , p2y + perpy); + verts.push(r, g, b, alpha); + + indices.push(indexStart); + + for (i = 0; i < indexCount; i++) + { + indices.push(indexStart++); + } + + indices.push(indexStart-1); +}; + +/** + * Builds a polygon to draw + * + * @static + * @private + * @method buildPoly + * @param graphicsData {Graphics} The graphics object containing all the necessary properties + * @param webGLData {Object} + */ +PIXI.WebGLGraphics.buildPoly = function(graphicsData, webGLData) +{ + var points = graphicsData.points; + if(points.length < 6)return; + + // get first and last point.. figure out the middle! + var verts = webGLData.points; + var indices = webGLData.indices; + + var length = points.length / 2; + + // sort color + var color = PIXI.hex2rgb(graphicsData.fillColor); + var alpha = graphicsData.fillAlpha; + var r = color[0] * alpha; + var g = color[1] * alpha; + var b = color[2] * alpha; + + var triangles = PIXI.PolyK.Triangulate(points); + + var vertPos = verts.length / 6; + + var i = 0; + + for (i = 0; i < triangles.length; i+=3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i] + vertPos); + indices.push(triangles[i+1] + vertPos); + indices.push(triangles[i+2] +vertPos); + indices.push(triangles[i+2] + vertPos); + } + + for (i = 0; i < length; i++) + { + verts.push(points[i * 2], points[i * 2 + 1], + r, g, b, alpha); + } +}; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +PIXI.glContexts = []; // this is where we store the webGL contexts for easy access. + +/** + * the WebGLRenderer draws the stage and all its content onto a webGL enabled canvas. This renderer + * should be used for browsers that support webGL. This Render works by automatically managing webGLBatch's. + * So no need for Sprite Batch's or Sprite Cloud's + * Dont forget to add the view to your DOM or you will not see anything :) + * + * @class WebGLRenderer + * @constructor + * @param width=0 {Number} the width of the canvas view + * @param height=0 {Number} the height of the canvas view + * @param view {HTMLCanvasElement} the canvas to use as a view, optional + * @param transparent=false {Boolean} If the render view is transparent, default false + * @param antialias=false {Boolean} sets antialias (only applicable in chrome at the moment) + * + */ +PIXI.WebGLRenderer = function(width, height, view, transparent, antialias) +{ + if(!PIXI.defaultRenderer)PIXI.defaultRenderer = this; + + this.type = PIXI.WEBGL_RENDERER; + + // do a catch.. only 1 webGL renderer.. + /** + * Whether the render view is transparent + * + * @property transparent + * @type Boolean + */ + this.transparent = !!transparent; + + /** + * The width of the canvas view + * + * @property width + * @type Number + * @default 800 + */ + this.width = width || 800; + + /** + * The height of the canvas view + * + * @property height + * @type Number + * @default 600 + */ + this.height = height || 600; + + /** + * The canvas element that everything is drawn to + * + * @property view + * @type HTMLCanvasElement + */ + this.view = view || document.createElement( 'canvas' ); + this.view.width = this.width; + this.view.height = this.height; + + // deal with losing context.. + this.contextLost = this.handleContextLost.bind(this); + this.contextRestoredLost = this.handleContextRestored.bind(this); + + this.view.addEventListener('webglcontextlost', this.contextLost, false); + this.view.addEventListener('webglcontextrestored', this.contextRestoredLost, false); + + this.options = { + alpha: this.transparent, + antialias:!!antialias, // SPEED UP?? + premultipliedAlpha:!!transparent, + stencil:true + }; + + //try 'experimental-webgl' + try { + this.gl = this.view.getContext('experimental-webgl', this.options); + } catch (e) { + //try 'webgl' + try { + this.gl = this.view.getContext('webgl', this.options); + } catch (e2) { + // fail, not able to get a context + throw new Error(' This browser does not support webGL. Try using the canvas renderer' + this); + } + } + + var gl = this.gl; + this.glContextId = gl.id = PIXI.WebGLRenderer.glContextId ++; + + PIXI.glContexts[this.glContextId] = gl; + + if(!PIXI.blendModesWebGL) + { + PIXI.blendModesWebGL = []; + + PIXI.blendModesWebGL[PIXI.blendModes.NORMAL] = [gl.ONE, gl.ONE_MINUS_SRC_ALPHA]; + PIXI.blendModesWebGL[PIXI.blendModes.ADD] = [gl.SRC_ALPHA, gl.DST_ALPHA]; + PIXI.blendModesWebGL[PIXI.blendModes.MULTIPLY] = [gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA]; + PIXI.blendModesWebGL[PIXI.blendModes.SCREEN] = [gl.SRC_ALPHA, gl.ONE]; + PIXI.blendModesWebGL[PIXI.blendModes.OVERLAY] = [gl.ONE, gl.ONE_MINUS_SRC_ALPHA]; + PIXI.blendModesWebGL[PIXI.blendModes.DARKEN] = [gl.ONE, gl.ONE_MINUS_SRC_ALPHA]; + PIXI.blendModesWebGL[PIXI.blendModes.LIGHTEN] = [gl.ONE, gl.ONE_MINUS_SRC_ALPHA]; + PIXI.blendModesWebGL[PIXI.blendModes.COLOR_DODGE] = [gl.ONE, gl.ONE_MINUS_SRC_ALPHA]; + PIXI.blendModesWebGL[PIXI.blendModes.COLOR_BURN] = [gl.ONE, gl.ONE_MINUS_SRC_ALPHA]; + PIXI.blendModesWebGL[PIXI.blendModes.HARD_LIGHT] = [gl.ONE, gl.ONE_MINUS_SRC_ALPHA]; + PIXI.blendModesWebGL[PIXI.blendModes.SOFT_LIGHT] = [gl.ONE, gl.ONE_MINUS_SRC_ALPHA]; + PIXI.blendModesWebGL[PIXI.blendModes.DIFFERENCE] = [gl.ONE, gl.ONE_MINUS_SRC_ALPHA]; + PIXI.blendModesWebGL[PIXI.blendModes.EXCLUSION] = [gl.ONE, gl.ONE_MINUS_SRC_ALPHA]; + PIXI.blendModesWebGL[PIXI.blendModes.HUE] = [gl.ONE, gl.ONE_MINUS_SRC_ALPHA]; + PIXI.blendModesWebGL[PIXI.blendModes.SATURATION] = [gl.ONE, gl.ONE_MINUS_SRC_ALPHA]; + PIXI.blendModesWebGL[PIXI.blendModes.COLOR] = [gl.ONE, gl.ONE_MINUS_SRC_ALPHA]; + PIXI.blendModesWebGL[PIXI.blendModes.LUMINOSITY] = [gl.ONE, gl.ONE_MINUS_SRC_ALPHA]; + } + + + + + this.projection = new PIXI.Point(); + this.projection.x = this.width/2; + this.projection.y = -this.height/2; + + this.offset = new PIXI.Point(0, 0); + + this.resize(this.width, this.height); + this.contextLost = false; + + // time to create the render managers! each one focuses on managine a state in webGL + this.shaderManager = new PIXI.WebGLShaderManager(gl); // deals with managing the shader programs and their attribs + this.spriteBatch = new PIXI.WebGLSpriteBatch(gl); // manages the rendering of sprites + this.maskManager = new PIXI.WebGLMaskManager(gl); // manages the masks using the stencil buffer + this.filterManager = new PIXI.WebGLFilterManager(gl, this.transparent); // manages the filters + + this.renderSession = {}; + this.renderSession.gl = this.gl; + this.renderSession.drawCount = 0; + this.renderSession.shaderManager = this.shaderManager; + this.renderSession.maskManager = this.maskManager; + this.renderSession.filterManager = this.filterManager; + this.renderSession.spriteBatch = this.spriteBatch; + this.renderSession.renderer = this; + + gl.useProgram(this.shaderManager.defaultShader.program); + + gl.disable(gl.DEPTH_TEST); + gl.disable(gl.CULL_FACE); + + gl.enable(gl.BLEND); + gl.colorMask(true, true, true, this.transparent); +}; + +// constructor +PIXI.WebGLRenderer.prototype.constructor = PIXI.WebGLRenderer; + +/** + * Renders the stage to its webGL view + * + * @method render + * @param stage {Stage} the Stage element to be rendered + */ +PIXI.WebGLRenderer.prototype.render = function(stage) +{ + if(this.contextLost)return; + + + // if rendering a new stage clear the batches.. + if(this.__stage !== stage) + { + if(stage.interactive)stage.interactionManager.removeEvents(); + + // TODO make this work + // dont think this is needed any more? + this.__stage = stage; + } + + // update any textures this includes uvs and uploading them to the gpu + PIXI.WebGLRenderer.updateTextures(); + + // update the scene graph + stage.updateTransform(); + + + // interaction + if(stage._interactive) + { + //need to add some events! + if(!stage._interactiveEventsAdded) + { + stage._interactiveEventsAdded = true; + stage.interactionManager.setTarget(this); + } + } + + var gl = this.gl; + + // -- Does this need to be set every frame? -- // + //gl.colorMask(true, true, true, this.transparent); + gl.viewport(0, 0, this.width, this.height); + + // make sure we are bound to the main frame buffer + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + if(this.transparent) + { + gl.clearColor(0, 0, 0, 0); + } + else + { + gl.clearColor(stage.backgroundColorSplit[0],stage.backgroundColorSplit[1],stage.backgroundColorSplit[2], 1); + } + + + gl.clear(gl.COLOR_BUFFER_BIT); + + this.renderDisplayObject( stage, this.projection ); + + // interaction + if(stage.interactive) + { + //need to add some events! + if(!stage._interactiveEventsAdded) + { + stage._interactiveEventsAdded = true; + stage.interactionManager.setTarget(this); + } + } + else + { + if(stage._interactiveEventsAdded) + { + stage._interactiveEventsAdded = false; + stage.interactionManager.setTarget(this); + } + } + + /* + //can simulate context loss in Chrome like so: + this.view.onmousedown = function(ev) { + console.dir(this.gl.getSupportedExtensions()); + var ext = ( + gl.getExtension("WEBGL_scompressed_texture_s3tc") + // gl.getExtension("WEBGL_compressed_texture_s3tc") || + // gl.getExtension("MOZ_WEBGL_compressed_texture_s3tc") || + // gl.getExtension("WEBKIT_WEBGL_compressed_texture_s3tc") + ); + console.dir(ext); + var loseCtx = this.gl.getExtension("WEBGL_lose_context"); + console.log("killing context"); + loseCtx.loseContext(); + setTimeout(function() { + console.log("restoring context..."); + loseCtx.restoreContext(); + }.bind(this), 1000); + }.bind(this); + */ +}; + +/** + * Renders a display Object + * + * @method renderDIsplayObject + * @param displayObject {DisplayObject} The DisplayObject to render + * @param projection {Point} The projection + * @param buffer {Array} a standard WebGL buffer + */ +PIXI.WebGLRenderer.prototype.renderDisplayObject = function(displayObject, projection, buffer) +{ + // reset the render session data.. + this.renderSession.drawCount = 0; + this.renderSession.currentBlendMode = 9999; + + this.renderSession.projection = projection; + this.renderSession.offset = this.offset; + + // start the sprite batch + this.spriteBatch.begin(this.renderSession); + + // start the filter manager + this.filterManager.begin(this.renderSession, buffer); + + // render the scene! + displayObject._renderWebGL(this.renderSession); + + // finish the sprite batch + this.spriteBatch.end(); +}; + +/** + * Updates the textures loaded into this webgl renderer + * + * @static + * @method updateTextures + * @private + */ +PIXI.WebGLRenderer.updateTextures = function() +{ + var i = 0; + + //TODO break this out into a texture manager... + //for (i = 0; i < PIXI.texturesToUpdate.length; i++) + // PIXI.WebGLRenderer.updateTexture(PIXI.texturesToUpdate[i]); + + + for (i=0; i < PIXI.Texture.frameUpdates.length; i++) + PIXI.WebGLRenderer.updateTextureFrame(PIXI.Texture.frameUpdates[i]); + + for (i = 0; i < PIXI.texturesToDestroy.length; i++) + PIXI.WebGLRenderer.destroyTexture(PIXI.texturesToDestroy[i]); + + PIXI.texturesToUpdate.length = 0; + PIXI.texturesToDestroy.length = 0; + PIXI.Texture.frameUpdates.length = 0; +}; + +/** + * Destroys a loaded webgl texture + * + * @method destroyTexture + * @param texture {Texture} The texture to update + * @private + */ +PIXI.WebGLRenderer.destroyTexture = function(texture) +{ + //TODO break this out into a texture manager... + + for (var i = texture._glTextures.length - 1; i >= 0; i--) + { + var glTexture = texture._glTextures[i]; + var gl = PIXI.glContexts[i]; + + if(gl && glTexture) + { + gl.deleteTexture(glTexture); + } + } + + texture._glTextures.length = 0; +}; + +/** + * + * @method updateTextureFrame + * @param texture {Texture} The texture to update the frame from + * @private + */ +PIXI.WebGLRenderer.updateTextureFrame = function(texture) +{ + texture.updateFrame = false; + + // now set the uvs. Figured that the uv data sits with a texture rather than a sprite. + // so uv data is stored on the texture itself + texture._updateWebGLuvs(); +}; + +/** + * resizes the webGL view to the specified width and height + * + * @method resize + * @param width {Number} the new width of the webGL view + * @param height {Number} the new height of the webGL view + */ +PIXI.WebGLRenderer.prototype.resize = function(width, height) +{ + this.width = width; + this.height = height; + + this.view.width = width; + this.view.height = height; + + this.gl.viewport(0, 0, this.width, this.height); + + this.projection.x = this.width/2; + this.projection.y = -this.height/2; +}; + +/** + * Creates a WebGL texture + * + * @method createWebGLTexture + * @param texture {Texture} the texture to render + * @param gl {webglContext} the WebGL context + * @static + */ +PIXI.createWebGLTexture = function(texture, gl) +{ + + + if(texture.hasLoaded) + { + texture._glTextures[gl.id] = gl.createTexture(); + + gl.bindTexture(gl.TEXTURE_2D, texture._glTextures[gl.id]); + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); + + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.source); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, texture.scaleMode === PIXI.scaleModes.LINEAR ? gl.LINEAR : gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, texture.scaleMode === PIXI.scaleModes.LINEAR ? gl.LINEAR : gl.NEAREST); + + // reguler... + + if(!texture._powerOf2) + { + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + } + else + { + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); + } + + gl.bindTexture(gl.TEXTURE_2D, null); + } + + return texture._glTextures[gl.id]; +}; + +/** + * Updates a WebGL texture + * + * @method updateWebGLTexture + * @param texture {Texture} the texture to update + * @param gl {webglContext} the WebGL context + * @private + */ +PIXI.updateWebGLTexture = function(texture, gl) +{ + if( texture._glTextures[gl.id] ) + { + gl.bindTexture(gl.TEXTURE_2D, texture._glTextures[gl.id]); + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); + + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.source); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, texture.scaleMode === PIXI.scaleModes.LINEAR ? gl.LINEAR : gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, texture.scaleMode === PIXI.scaleModes.LINEAR ? gl.LINEAR : gl.NEAREST); + + // reguler... + + if(!texture._powerOf2) + { + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + } + else + { + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); + } + + gl.bindTexture(gl.TEXTURE_2D, null); + } + +}; + +/** + * Handles a lost webgl context + * + * @method handleContextLost + * @param event {Event} + * @private + */ +PIXI.WebGLRenderer.prototype.handleContextLost = function(event) +{ + event.preventDefault(); + this.contextLost = true; +}; + +/** + * Handles a restored webgl context + * + * @method handleContextRestored + * @param event {Event} + * @private + */ +PIXI.WebGLRenderer.prototype.handleContextRestored = function() +{ + + //try 'experimental-webgl' + try { + this.gl = this.view.getContext('experimental-webgl', this.options); + } catch (e) { + //try 'webgl' + try { + this.gl = this.view.getContext('webgl', this.options); + } catch (e2) { + // fail, not able to get a context + throw new Error(' This browser does not support webGL. Try using the canvas renderer' + this); + } + } + + var gl = this.gl; + gl.id = PIXI.WebGLRenderer.glContextId ++; + + + + // need to set the context... + this.shaderManager.setContext(gl); + this.spriteBatch.setContext(gl); + this.maskManager.setContext(gl); + this.filterManager.setContext(gl); + + + this.renderSession.gl = this.gl; + + gl.disable(gl.DEPTH_TEST); + gl.disable(gl.CULL_FACE); + + gl.enable(gl.BLEND); + gl.colorMask(true, true, true, this.transparent); + + this.gl.viewport(0, 0, this.width, this.height); + + for(var key in PIXI.TextureCache) + { + var texture = PIXI.TextureCache[key].baseTexture; + texture._glTextures = []; + } + + /** + * Whether the context was lost + * @property contextLost + * @type Boolean + */ + this.contextLost = false; + +}; + +/** + * Removes everything from the renderer (event listeners, spritebatch, etc...) + * + * @method destroy + */ +PIXI.WebGLRenderer.prototype.destroy = function() +{ + + // deal with losing context.. + + // remove listeners + this.view.removeEventListener('webglcontextlost', this.contextLost); + this.view.removeEventListener('webglcontextrestored', this.contextRestoredLost); + + PIXI.glContexts[this.glContextId] = null; + + this.projection = null; + this.offset = null; + + // time to create the render managers! each one focuses on managine a state in webGL + this.shaderManager.destroy(); + this.spriteBatch.destroy(); + this.maskManager.destroy(); + this.filterManager.destroy(); + + this.shaderManager = null; + this.spriteBatch = null; + this.maskManager = null; + this.filterManager = null; + + this.gl = null; + // + this.renderSession = null; +}; + + +PIXI.WebGLRenderer.glContextId = 0; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + + +/** +* @class WebGLMaskManager +* @constructor +* @param gl {WebGLContext} the current WebGL drawing context +* @private +*/ +PIXI.WebGLMaskManager = function(gl) +{ + this.maskStack = []; + this.maskPosition = 0; + + this.setContext(gl); +}; + +/** +* Sets the drawing context to the one given in parameter +* @method setContext +* @param gl {WebGLContext} the current WebGL drawing context +*/ +PIXI.WebGLMaskManager.prototype.setContext = function(gl) +{ + this.gl = gl; +}; + +/** +* Applies the Mask and adds it to the current filter stack +* @method pushMask +* @param maskData {Array} +* @param renderSession {RenderSession} +*/ +PIXI.WebGLMaskManager.prototype.pushMask = function(maskData, renderSession) +{ + var gl = this.gl; + + if(this.maskStack.length === 0) + { + gl.enable(gl.STENCIL_TEST); + gl.stencilFunc(gl.ALWAYS,1,1); + } + + // maskData.visible = false; + + this.maskStack.push(maskData); + + gl.colorMask(false, false, false, false); + gl.stencilOp(gl.KEEP,gl.KEEP,gl.INCR); + + PIXI.WebGLGraphics.renderGraphics(maskData, renderSession); + + gl.colorMask(true, true, true, true); + gl.stencilFunc(gl.NOTEQUAL,0, this.maskStack.length); + gl.stencilOp(gl.KEEP,gl.KEEP,gl.KEEP); +}; + +/** +* Removes the last filter from the filter stack and doesn't return it +* @method popMask +* +* @param renderSession {RenderSession} an object containing all the useful parameters +*/ +PIXI.WebGLMaskManager.prototype.popMask = function(renderSession) +{ + var gl = this.gl; + + var maskData = this.maskStack.pop(); + + if(maskData) + { + gl.colorMask(false, false, false, false); + + //gl.stencilFunc(gl.ALWAYS,1,1); + gl.stencilOp(gl.KEEP,gl.KEEP,gl.DECR); + + PIXI.WebGLGraphics.renderGraphics(maskData, renderSession); + + gl.colorMask(true, true, true, true); + gl.stencilFunc(gl.NOTEQUAL,0,this.maskStack.length); + gl.stencilOp(gl.KEEP,gl.KEEP,gl.KEEP); + } + + if(this.maskStack.length === 0)gl.disable(gl.STENCIL_TEST); +}; + +/** +* Destroys the mask stack +* @method destroy +*/ +PIXI.WebGLMaskManager.prototype.destroy = function() +{ + this.maskStack = null; + this.gl = null; +}; +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +/** +* @class WebGLShaderManager +* @constructor +* @param gl {WebGLContext} the current WebGL drawing context +* @private +*/ +PIXI.WebGLShaderManager = function(gl) +{ + + this.maxAttibs = 10; + this.attribState = []; + this.tempAttribState = []; + + for (var i = 0; i < this.maxAttibs; i++) { + this.attribState[i] = false; + } + + this.setContext(gl); + // the final one is used for the rendering strips + //this.stripShader = new PIXI.StripShader(gl); +}; + + +/** +* Initialises the context and the properties +* @method setContext +* @param gl {WebGLContext} the current WebGL drawing context +* @param transparent {Boolean} Whether or not the drawing context should be transparent +*/ +PIXI.WebGLShaderManager.prototype.setContext = function(gl) +{ + this.gl = gl; + + // the next one is used for rendering primatives + this.primitiveShader = new PIXI.PrimitiveShader(gl); + + // this shader is used for the default sprite rendering + this.defaultShader = new PIXI.PixiShader(gl); + + // this shader is used for the fast sprite rendering + this.fastShader = new PIXI.PixiFastShader(gl); + + + this.activateShader(this.defaultShader); +}; + + +/** +* Takes the attributes given in parameters +* @method setAttribs +* @param attribs {Array} attribs +*/ +PIXI.WebGLShaderManager.prototype.setAttribs = function(attribs) +{ + // reset temp state + + var i; + + for (i = 0; i < this.tempAttribState.length; i++) + { + this.tempAttribState[i] = false; + } + + // set the new attribs + for (i = 0; i < attribs.length; i++) + { + var attribId = attribs[i]; + this.tempAttribState[attribId] = true; + } + + var gl = this.gl; + + for (i = 0; i < this.attribState.length; i++) + { + + if(this.attribState[i] !== this.tempAttribState[i]) + { + this.attribState[i] = this.tempAttribState[i]; + + if(this.tempAttribState[i]) + { + gl.enableVertexAttribArray(i); + } + else + { + gl.disableVertexAttribArray(i); + } + } + } +}; + +/** +* Sets-up the given shader +* +* @method activateShader +* @param shader {Object} the shader that is going to be activated +*/ +PIXI.WebGLShaderManager.prototype.activateShader = function(shader) +{ + //if(this.currentShader == shader)return; + + this.currentShader = shader; + + this.gl.useProgram(shader.program); + this.setAttribs(shader.attributes); + +}; + +/** +* Triggers the primitive shader +* @method activatePrimitiveShader +*/ +PIXI.WebGLShaderManager.prototype.activatePrimitiveShader = function() +{ + var gl = this.gl; + + gl.useProgram(this.primitiveShader.program); + + this.setAttribs(this.primitiveShader.attributes); + +}; + +/** +* Disable the primitive shader +* @method deactivatePrimitiveShader +*/ +PIXI.WebGLShaderManager.prototype.deactivatePrimitiveShader = function() +{ + var gl = this.gl; + + gl.useProgram(this.defaultShader.program); + + this.setAttribs(this.defaultShader.attributes); +}; + +/** +* Destroys +* @method destroy +*/ +PIXI.WebGLShaderManager.prototype.destroy = function() +{ + this.attribState = null; + + this.tempAttribState = null; + + this.primitiveShader.destroy(); + + this.defaultShader.destroy(); + + this.fastShader.destroy(); + + this.gl = null; +}; + + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original pixi version! + * + * Heavily inspired by LibGDX's WebGLSpriteBatch: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/WebGLSpriteBatch.java + */ + + /** + * + * @class WebGLSpriteBatch + * @private + * @constructor + * @param gl {WebGLContext} the current WebGL drawing context + * + */ +PIXI.WebGLSpriteBatch = function(gl) +{ + + /** + * + * + * @property vertSize + * @type Number + */ + this.vertSize = 6; + + /** + * The number of images in the SpriteBatch before it flushes + * @property size + * @type Number + */ + this.size = 2000;//Math.pow(2, 16) / this.vertSize; + + //the total number of floats in our batch + var numVerts = this.size * 4 * this.vertSize; + //the total number of indices in our batch + var numIndices = this.size * 6; + + //vertex data + + /** + * Holds the vertices + * + * @property vertices + * @type Float32Array + */ + this.vertices = new Float32Array(numVerts); + + //index data + /** + * Holds the indices + * + * @property indices + * @type Uint16Array + */ + this.indices = new Uint16Array(numIndices); + + this.lastIndexCount = 0; + + for (var i=0, j=0; i < numIndices; i += 6, j += 4) + { + this.indices[i + 0] = j + 0; + this.indices[i + 1] = j + 1; + this.indices[i + 2] = j + 2; + this.indices[i + 3] = j + 0; + this.indices[i + 4] = j + 2; + this.indices[i + 5] = j + 3; + } + + + this.drawing = false; + this.currentBatchSize = 0; + this.currentBaseTexture = null; + + this.setContext(gl); +}; + +/** +* +* @method setContext +* +* @param gl {WebGLContext} the current WebGL drawing context +*/ +PIXI.WebGLSpriteBatch.prototype.setContext = function(gl) +{ + this.gl = gl; + + // create a couple of buffers + this.vertexBuffer = gl.createBuffer(); + this.indexBuffer = gl.createBuffer(); + + // 65535 is max index, so 65535 / 6 = 10922. + + + //upload the index data + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.indices, gl.STATIC_DRAW); + + gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); + gl.bufferData(gl.ARRAY_BUFFER, this.vertices, gl.DYNAMIC_DRAW); + + this.currentBlendMode = 99999; +}; + +/** +* +* @method begin +* +* @param renderSession {RenderSession} the RenderSession +*/ +PIXI.WebGLSpriteBatch.prototype.begin = function(renderSession) +{ + this.renderSession = renderSession; + this.shader = this.renderSession.shaderManager.defaultShader; + + this.start(); +}; + +/** +* +* @method end +* +*/ +PIXI.WebGLSpriteBatch.prototype.end = function() +{ + this.flush(); +}; + +/** +* +* @method render +* +* @param sprite {Sprite} the sprite to render when using this spritebatch +*/ +PIXI.WebGLSpriteBatch.prototype.render = function(sprite) +{ + var texture = sprite.texture; + + // check texture.. + if(texture.baseTexture !== this.currentBaseTexture || this.currentBatchSize >= this.size) + { + this.flush(); + this.currentBaseTexture = texture.baseTexture; + } + + + // check blend mode + if(sprite.blendMode !== this.currentBlendMode) + { + this.setBlendMode(sprite.blendMode); + } + + // get the uvs for the texture + var uvs = sprite._uvs || sprite.texture._uvs; + // if the uvs have not updated then no point rendering just yet! + if(!uvs)return; + + // get the sprites current alpha + var alpha = sprite.worldAlpha; + var tint = sprite.tint; + + var verticies = this.vertices; + + + // TODO trim?? + var aX = sprite.anchor.x; + var aY = sprite.anchor.y; + + var w0, w1, h0, h1; + + if (sprite.texture.trim) + { + // if the sprite is trimmed then we need to add the extra space before transforming the sprite coords.. + var trim = sprite.texture.trim; + + w1 = trim.x - aX * trim.width; + w0 = w1 + texture.frame.width; + + h1 = trim.y - aY * trim.height; + h0 = h1 + texture.frame.height; + + } + else + { + w0 = (texture.frame.width ) * (1-aX); + w1 = (texture.frame.width ) * -aX; + + h0 = texture.frame.height * (1-aY); + h1 = texture.frame.height * -aY; + } + + var index = this.currentBatchSize * 4 * this.vertSize; + + var worldTransform = sprite.worldTransform;//.toArray(); + + var a = worldTransform.a;//[0]; + var b = worldTransform.c;//[3]; + var c = worldTransform.b;//[1]; + var d = worldTransform.d;//[4]; + var tx = worldTransform.tx;//[2]; + var ty = worldTransform.ty;///[5]; + + // xy + verticies[index++] = a * w1 + c * h1 + tx; + verticies[index++] = d * h1 + b * w1 + ty; + // uv + verticies[index++] = uvs.x0; + verticies[index++] = uvs.y0; + // color + verticies[index++] = alpha; + verticies[index++] = tint; + + // xy + verticies[index++] = a * w0 + c * h1 + tx; + verticies[index++] = d * h1 + b * w0 + ty; + // uv + verticies[index++] = uvs.x1; + verticies[index++] = uvs.y1; + // color + verticies[index++] = alpha; + verticies[index++] = tint; + + // xy + verticies[index++] = a * w0 + c * h0 + tx; + verticies[index++] = d * h0 + b * w0 + ty; + // uv + verticies[index++] = uvs.x2; + verticies[index++] = uvs.y2; + // color + verticies[index++] = alpha; + verticies[index++] = tint; + + // xy + verticies[index++] = a * w1 + c * h0 + tx; + verticies[index++] = d * h0 + b * w1 + ty; + // uv + verticies[index++] = uvs.x3; + verticies[index++] = uvs.y3; + // color + verticies[index++] = alpha; + verticies[index++] = tint; + + // increment the batchsize + this.currentBatchSize++; + + +}; + +/** +* Renders a tilingSprite using the spriteBatch +* @method renderTilingSprite +* +* @param sprite {TilingSprite} the tilingSprite to render +*/ +PIXI.WebGLSpriteBatch.prototype.renderTilingSprite = function(tilingSprite) +{ + var texture = tilingSprite.tilingTexture; + + if(texture.baseTexture !== this.currentBaseTexture || this.currentBatchSize >= this.size) + { + this.flush(); + this.currentBaseTexture = texture.baseTexture; + } + + // check blend mode + if(tilingSprite.blendMode !== this.currentBlendMode) + { + this.setBlendMode(tilingSprite.blendMode); + } + + // set the textures uvs temporarily + // TODO create a separate texture so that we can tile part of a texture + + if(!tilingSprite._uvs)tilingSprite._uvs = new PIXI.TextureUvs(); + + var uvs = tilingSprite._uvs; + + tilingSprite.tilePosition.x %= texture.baseTexture.width * tilingSprite.tileScaleOffset.x; + tilingSprite.tilePosition.y %= texture.baseTexture.height * tilingSprite.tileScaleOffset.y; + + var offsetX = tilingSprite.tilePosition.x/(texture.baseTexture.width*tilingSprite.tileScaleOffset.x); + var offsetY = tilingSprite.tilePosition.y/(texture.baseTexture.height*tilingSprite.tileScaleOffset.y); + + var scaleX = (tilingSprite.width / texture.baseTexture.width) / (tilingSprite.tileScale.x * tilingSprite.tileScaleOffset.x); + var scaleY = (tilingSprite.height / texture.baseTexture.height) / (tilingSprite.tileScale.y * tilingSprite.tileScaleOffset.y); + + uvs.x0 = 0 - offsetX; + uvs.y0 = 0 - offsetY; + + uvs.x1 = (1 * scaleX) - offsetX; + uvs.y1 = 0 - offsetY; + + uvs.x2 = (1 * scaleX) - offsetX; + uvs.y2 = (1 * scaleY) - offsetY; + + uvs.x3 = 0 - offsetX; + uvs.y3 = (1 *scaleY) - offsetY; + + // get the tilingSprites current alpha + var alpha = tilingSprite.worldAlpha; + var tint = tilingSprite.tint; + + var verticies = this.vertices; + + var width = tilingSprite.width; + var height = tilingSprite.height; + + // TODO trim?? + var aX = tilingSprite.anchor.x; // - tilingSprite.texture.trim.x + var aY = tilingSprite.anchor.y; //- tilingSprite.texture.trim.y + var w0 = width * (1-aX); + var w1 = width * -aX; + + var h0 = height * (1-aY); + var h1 = height * -aY; + + var index = this.currentBatchSize * 4 * this.vertSize; + + var worldTransform = tilingSprite.worldTransform; + + var a = worldTransform.a;//[0]; + var b = worldTransform.c;//[3]; + var c = worldTransform.b;//[1]; + var d = worldTransform.d;//[4]; + var tx = worldTransform.tx;//[2]; + var ty = worldTransform.ty;///[5]; + + // xy + verticies[index++] = a * w1 + c * h1 + tx; + verticies[index++] = d * h1 + b * w1 + ty; + // uv + verticies[index++] = uvs.x0; + verticies[index++] = uvs.y0; + // color + verticies[index++] = alpha; + verticies[index++] = tint; + + // xy + verticies[index++] = a * w0 + c * h1 + tx; + verticies[index++] = d * h1 + b * w0 + ty; + // uv + verticies[index++] = uvs.x1; + verticies[index++] = uvs.y1; + // color + verticies[index++] = alpha; + verticies[index++] = tint; + + // xy + verticies[index++] = a * w0 + c * h0 + tx; + verticies[index++] = d * h0 + b * w0 + ty; + // uv + verticies[index++] = uvs.x2; + verticies[index++] = uvs.y2; + // color + verticies[index++] = alpha; + verticies[index++] = tint; + + // xy + verticies[index++] = a * w1 + c * h0 + tx; + verticies[index++] = d * h0 + b * w1 + ty; + // uv + verticies[index++] = uvs.x3; + verticies[index++] = uvs.y3; + // color + verticies[index++] = alpha; + verticies[index++] = tint; + + // increment the batchs + this.currentBatchSize++; +}; + + +/** +* Renders the content and empties the current batch +* +* @method flush +* +*/ +PIXI.WebGLSpriteBatch.prototype.flush = function() +{ + // If the batch is length 0 then return as there is nothing to draw + if (this.currentBatchSize===0)return; + + var gl = this.gl; + + // bind the current texture + gl.bindTexture(gl.TEXTURE_2D, this.currentBaseTexture._glTextures[gl.id] || PIXI.createWebGLTexture(this.currentBaseTexture, gl)); + + // upload the verts to the buffer + + if(this.currentBatchSize > ( this.size * 0.5 ) ) + { + gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.vertices); + } + else + { + var view = this.vertices.subarray(0, this.currentBatchSize * 4 * this.vertSize); + + gl.bufferSubData(gl.ARRAY_BUFFER, 0, view); + } + + // var view = this.vertices.subarray(0, this.currentBatchSize * 4 * this.vertSize); + //gl.bufferSubData(gl.ARRAY_BUFFER, 0, view); + + // now draw those suckas! + gl.drawElements(gl.TRIANGLES, this.currentBatchSize * 6, gl.UNSIGNED_SHORT, 0); + + // then reset the batch! + this.currentBatchSize = 0; + + // increment the draw count + this.renderSession.drawCount++; +}; + +/** +* +* @method stop +* +*/ +PIXI.WebGLSpriteBatch.prototype.stop = function() +{ + this.flush(); +}; + +/** +* +* @method start +* +*/ +PIXI.WebGLSpriteBatch.prototype.start = function() +{ + var gl = this.gl; + + // bind the main texture + gl.activeTexture(gl.TEXTURE0); + + // bind the buffers + gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); + + // set the projection + var projection = this.renderSession.projection; + gl.uniform2f(this.shader.projectionVector, projection.x, projection.y); + + // set the pointers + var stride = this.vertSize * 4; + gl.vertexAttribPointer(this.shader.aVertexPosition, 2, gl.FLOAT, false, stride, 0); + gl.vertexAttribPointer(this.shader.aTextureCoord, 2, gl.FLOAT, false, stride, 2 * 4); + gl.vertexAttribPointer(this.shader.colorAttribute, 2, gl.FLOAT, false, stride, 4 * 4); + + // set the blend mode.. + if(this.currentBlendMode !== PIXI.blendModes.NORMAL) + { + this.setBlendMode(PIXI.blendModes.NORMAL); + } +}; + +/** +* Sets-up the given blendMode from WebGL's point of view +* @method setBlendMode +* +* @param blendMode {Number} the blendMode, should be a Pixi const, such as PIXI.BlendModes.ADD +*/ +PIXI.WebGLSpriteBatch.prototype.setBlendMode = function(blendMode) +{ + this.flush(); + + this.currentBlendMode = blendMode; + + var blendModeWebGL = PIXI.blendModesWebGL[this.currentBlendMode]; + this.gl.blendFunc(blendModeWebGL[0], blendModeWebGL[1]); +}; + +/** +* Destroys the SpriteBatch +* @method destroy +*/ +PIXI.WebGLSpriteBatch.prototype.destroy = function() +{ + + this.vertices = null; + this.indices = null; + + this.gl.deleteBuffer( this.vertexBuffer ); + this.gl.deleteBuffer( this.indexBuffer ); + + this.currentBaseTexture = null; + + this.gl = null; +}; + + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original pixi version! + * + * Heavily inspired by LibGDX's WebGLSpriteBatch: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/WebGLSpriteBatch.java + */ + +PIXI.WebGLFastSpriteBatch = function(gl) +{ + + + this.vertSize = 10; + this.maxSize = 6000;//Math.pow(2, 16) / this.vertSize; + this.size = this.maxSize; + + //the total number of floats in our batch + var numVerts = this.size * 4 * this.vertSize; + //the total number of indices in our batch + var numIndices = this.maxSize * 6; + + //vertex data + this.vertices = new Float32Array(numVerts); + //index data + this.indices = new Uint16Array(numIndices); + + this.vertexBuffer = null; + this.indexBuffer = null; + + this.lastIndexCount = 0; + + for (var i=0, j=0; i < numIndices; i += 6, j += 4) + { + this.indices[i + 0] = j + 0; + this.indices[i + 1] = j + 1; + this.indices[i + 2] = j + 2; + this.indices[i + 3] = j + 0; + this.indices[i + 4] = j + 2; + this.indices[i + 5] = j + 3; + } + + this.drawing = false; + this.currentBatchSize = 0; + this.currentBaseTexture = null; + + this.currentBlendMode = 0; + this.renderSession = null; + + + this.shader = null; + + this.matrix = null; + + this.setContext(gl); +}; + +PIXI.WebGLFastSpriteBatch.prototype.setContext = function(gl) +{ + this.gl = gl; + + // create a couple of buffers + this.vertexBuffer = gl.createBuffer(); + this.indexBuffer = gl.createBuffer(); + + // 65535 is max index, so 65535 / 6 = 10922. + + + //upload the index data + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.indices, gl.STATIC_DRAW); + + gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); + gl.bufferData(gl.ARRAY_BUFFER, this.vertices, gl.DYNAMIC_DRAW); + + this.currentBlendMode = 99999; +}; + +PIXI.WebGLFastSpriteBatch.prototype.begin = function(spriteBatch, renderSession) +{ + this.renderSession = renderSession; + this.shader = this.renderSession.shaderManager.fastShader; + + this.matrix = spriteBatch.worldTransform.toArray(true); + + this.start(); +}; + +PIXI.WebGLFastSpriteBatch.prototype.end = function() +{ + this.flush(); +}; + + +PIXI.WebGLFastSpriteBatch.prototype.render = function(spriteBatch) +{ + + var children = spriteBatch.children; + var sprite = children[0]; + + // if the uvs have not updated then no point rendering just yet! + + // check texture. + if(!sprite.texture._uvs)return; + + this.currentBaseTexture = sprite.texture.baseTexture; + // check blend mode + if(sprite.blendMode !== this.currentBlendMode) + { + this.setBlendMode(sprite.blendMode); + } + + for(var i=0,j= children.length; i= this.size) + { + this.flush(); + } +}; + +PIXI.WebGLFastSpriteBatch.prototype.flush = function() +{ + + // If the batch is length 0 then return as there is nothing to draw + if (this.currentBatchSize===0)return; + + var gl = this.gl; + + // bind the current texture + + if(!this.currentBaseTexture._glTextures[gl.id])PIXI.createWebGLTexture(this.currentBaseTexture, gl); + + gl.bindTexture(gl.TEXTURE_2D, this.currentBaseTexture._glTextures[gl.id]);// || PIXI.createWebGLTexture(this.currentBaseTexture, gl)); + + // upload the verts to the buffer + + + if(this.currentBatchSize > ( this.size * 0.5 ) ) + { + gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.vertices); + } + else + { + var view = this.vertices.subarray(0, this.currentBatchSize * 4 * this.vertSize); + + gl.bufferSubData(gl.ARRAY_BUFFER, 0, view); + } + + + // now draw those suckas! + gl.drawElements(gl.TRIANGLES, this.currentBatchSize * 6, gl.UNSIGNED_SHORT, 0); + + // then reset the batch! + this.currentBatchSize = 0; + + // increment the draw count + this.renderSession.drawCount++; +}; + + +PIXI.WebGLFastSpriteBatch.prototype.stop = function() +{ + this.flush(); +}; + +PIXI.WebGLFastSpriteBatch.prototype.start = function() +{ + var gl = this.gl; + + // bind the main texture + gl.activeTexture(gl.TEXTURE0); + + // bind the buffers + gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); + + // set the projection + var projection = this.renderSession.projection; + gl.uniform2f(this.shader.projectionVector, projection.x, projection.y); + + // set the matrix + gl.uniformMatrix3fv(this.shader.uMatrix, false, this.matrix); + + // set the pointers + var stride = this.vertSize * 4; + + gl.vertexAttribPointer(this.shader.aVertexPosition, 2, gl.FLOAT, false, stride, 0); + gl.vertexAttribPointer(this.shader.aPositionCoord, 2, gl.FLOAT, false, stride, 2 * 4); + gl.vertexAttribPointer(this.shader.aScale, 2, gl.FLOAT, false, stride, 4 * 4); + gl.vertexAttribPointer(this.shader.aRotation, 1, gl.FLOAT, false, stride, 6 * 4); + gl.vertexAttribPointer(this.shader.aTextureCoord, 2, gl.FLOAT, false, stride, 7 * 4); + gl.vertexAttribPointer(this.shader.colorAttribute, 1, gl.FLOAT, false, stride, 9 * 4); + + // set the blend mode.. + if(this.currentBlendMode !== PIXI.blendModes.NORMAL) + { + this.setBlendMode(PIXI.blendModes.NORMAL); + } +}; + +PIXI.WebGLFastSpriteBatch.prototype.setBlendMode = function(blendMode) +{ + this.flush(); + + this.currentBlendMode = blendMode; + + var blendModeWebGL = PIXI.blendModesWebGL[this.currentBlendMode]; + this.gl.blendFunc(blendModeWebGL[0], blendModeWebGL[1]); +}; + + + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +/** +* @class WebGLFilterManager +* @constructor +* @param gl {WebGLContext} the current WebGL drawing context +* @param transparent {Boolean} Whether or not the drawing context should be transparent +* @private +*/ +PIXI.WebGLFilterManager = function(gl, transparent) +{ + this.transparent = transparent; + + this.filterStack = []; + + this.offsetX = 0; + this.offsetY = 0; + + this.setContext(gl); +}; + +// API +/** +* Initialises the context and the properties +* @method setContext +* @param gl {WebGLContext} the current WebGL drawing context +*/ +PIXI.WebGLFilterManager.prototype.setContext = function(gl) +{ + this.gl = gl; + this.texturePool = []; + + this.initShaderBuffers(); +}; + +/** +* +* @method begin +* @param renderSession {RenderSession} +* @param buffer {ArrayBuffer} +*/ +PIXI.WebGLFilterManager.prototype.begin = function(renderSession, buffer) +{ + this.renderSession = renderSession; + this.defaultShader = renderSession.shaderManager.defaultShader; + + var projection = this.renderSession.projection; + // console.log(this.width) + this.width = projection.x * 2; + this.height = -projection.y * 2; + this.buffer = buffer; +}; + +/** +* Applies the filter and adds it to the current filter stack +* @method pushFilter +* @param filterBlock {Object} the filter that will be pushed to the current filter stack +*/ +PIXI.WebGLFilterManager.prototype.pushFilter = function(filterBlock) +{ + var gl = this.gl; + + var projection = this.renderSession.projection; + var offset = this.renderSession.offset; + + filterBlock._filterArea = filterBlock.target.filterArea || filterBlock.target.getBounds(); + + + // filter program + // OPTIMISATION - the first filter is free if its a simple color change? + this.filterStack.push(filterBlock); + + var filter = filterBlock.filterPasses[0]; + + this.offsetX += filterBlock._filterArea.x; + this.offsetY += filterBlock._filterArea.y; + + var texture = this.texturePool.pop(); + if(!texture) + { + texture = new PIXI.FilterTexture(this.gl, this.width, this.height); + } + else + { + texture.resize(this.width, this.height); + } + + gl.bindTexture(gl.TEXTURE_2D, texture.texture); + + var filterArea = filterBlock._filterArea;// filterBlock.target.getBounds();///filterBlock.target.filterArea; + + var padding = filter.padding; + filterArea.x -= padding; + filterArea.y -= padding; + filterArea.width += padding * 2; + filterArea.height += padding * 2; + + // cap filter to screen size.. + if(filterArea.x < 0)filterArea.x = 0; + if(filterArea.width > this.width)filterArea.width = this.width; + if(filterArea.y < 0)filterArea.y = 0; + if(filterArea.height > this.height)filterArea.height = this.height; + + //gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, filterArea.width, filterArea.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + gl.bindFramebuffer(gl.FRAMEBUFFER, texture.frameBuffer); + + // set view port + gl.viewport(0, 0, filterArea.width, filterArea.height); + + projection.x = filterArea.width/2; + projection.y = -filterArea.height/2; + + offset.x = -filterArea.x; + offset.y = -filterArea.y; + + // update projection + gl.uniform2f(this.defaultShader.projectionVector, filterArea.width/2, -filterArea.height/2); + gl.uniform2f(this.defaultShader.offsetVector, -filterArea.x, -filterArea.y); + + gl.colorMask(true, true, true, true); + gl.clearColor(0,0,0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); + + filterBlock._glFilterTexture = texture; + +}; + + +/** +* Removes the last filter from the filter stack and doesn't return it +* @method popFilter +*/ +PIXI.WebGLFilterManager.prototype.popFilter = function() +{ + var gl = this.gl; + var filterBlock = this.filterStack.pop(); + var filterArea = filterBlock._filterArea; + var texture = filterBlock._glFilterTexture; + var projection = this.renderSession.projection; + var offset = this.renderSession.offset; + + if(filterBlock.filterPasses.length > 1) + { + gl.viewport(0, 0, filterArea.width, filterArea.height); + + gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); + + this.vertexArray[0] = 0; + this.vertexArray[1] = filterArea.height; + + this.vertexArray[2] = filterArea.width; + this.vertexArray[3] = filterArea.height; + + this.vertexArray[4] = 0; + this.vertexArray[5] = 0; + + this.vertexArray[6] = filterArea.width; + this.vertexArray[7] = 0; + + gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.vertexArray); + + gl.bindBuffer(gl.ARRAY_BUFFER, this.uvBuffer); + // now set the uvs.. + this.uvArray[2] = filterArea.width/this.width; + this.uvArray[5] = filterArea.height/this.height; + this.uvArray[6] = filterArea.width/this.width; + this.uvArray[7] = filterArea.height/this.height; + + gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.uvArray); + + var inputTexture = texture; + var outputTexture = this.texturePool.pop(); + if(!outputTexture)outputTexture = new PIXI.FilterTexture(this.gl, this.width, this.height); + outputTexture.resize(this.width, this.height); + + // need to clear this FBO as it may have some left over elements from a previous filter. + gl.bindFramebuffer(gl.FRAMEBUFFER, outputTexture.frameBuffer ); + gl.clear(gl.COLOR_BUFFER_BIT); + + gl.disable(gl.BLEND); + + for (var i = 0; i < filterBlock.filterPasses.length-1; i++) + { + var filterPass = filterBlock.filterPasses[i]; + + gl.bindFramebuffer(gl.FRAMEBUFFER, outputTexture.frameBuffer ); + + // set texture + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, inputTexture.texture); + + // draw texture.. + //filterPass.applyFilterPass(filterArea.width, filterArea.height); + this.applyFilterPass(filterPass, filterArea, filterArea.width, filterArea.height); + + // swap the textures.. + var temp = inputTexture; + inputTexture = outputTexture; + outputTexture = temp; + } + + gl.enable(gl.BLEND); + + texture = inputTexture; + this.texturePool.push(outputTexture); + } + + var filter = filterBlock.filterPasses[filterBlock.filterPasses.length-1]; + + this.offsetX -= filterArea.x; + this.offsetY -= filterArea.y; + + + var sizeX = this.width; + var sizeY = this.height; + + var offsetX = 0; + var offsetY = 0; + + var buffer = this.buffer; + + // time to render the filters texture to the previous scene + if(this.filterStack.length === 0) + { + gl.colorMask(true, true, true, true);//this.transparent); + } + else + { + var currentFilter = this.filterStack[this.filterStack.length-1]; + filterArea = currentFilter._filterArea; + + sizeX = filterArea.width; + sizeY = filterArea.height; + + offsetX = filterArea.x; + offsetY = filterArea.y; + + buffer = currentFilter._glFilterTexture.frameBuffer; + } + + + + // TODO need toremove thease global elements.. + projection.x = sizeX/2; + projection.y = -sizeY/2; + + offset.x = offsetX; + offset.y = offsetY; + + filterArea = filterBlock._filterArea; + + var x = filterArea.x-offsetX; + var y = filterArea.y-offsetY; + + // update the buffers.. + // make sure to flip the y! + gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); + + this.vertexArray[0] = x; + this.vertexArray[1] = y + filterArea.height; + + this.vertexArray[2] = x + filterArea.width; + this.vertexArray[3] = y + filterArea.height; + + this.vertexArray[4] = x; + this.vertexArray[5] = y; + + this.vertexArray[6] = x + filterArea.width; + this.vertexArray[7] = y; + + gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.vertexArray); + + gl.bindBuffer(gl.ARRAY_BUFFER, this.uvBuffer); + + this.uvArray[2] = filterArea.width/this.width; + this.uvArray[5] = filterArea.height/this.height; + this.uvArray[6] = filterArea.width/this.width; + this.uvArray[7] = filterArea.height/this.height; + + gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.uvArray); + + //console.log(this.vertexArray) + //console.log(this.uvArray) + //console.log(sizeX + " : " + sizeY) + + gl.viewport(0, 0, sizeX, sizeY); + + // bind the buffer + gl.bindFramebuffer(gl.FRAMEBUFFER, buffer ); + + // set the blend mode! + //gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA) + + // set texture + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, texture.texture); + + // apply! + this.applyFilterPass(filter, filterArea, sizeX, sizeY); + + // now restore the regular shader.. + gl.useProgram(this.defaultShader.program); + gl.uniform2f(this.defaultShader.projectionVector, sizeX/2, -sizeY/2); + gl.uniform2f(this.defaultShader.offsetVector, -offsetX, -offsetY); + + // return the texture to the pool + this.texturePool.push(texture); + filterBlock._glFilterTexture = null; +}; + + +/** +* Applies the filter to the specified area +* @method applyFilterPass +* @param filter {AbstractFilter} the filter that needs to be applied +* @param filterArea {texture} TODO - might need an update +* @param width {Number} the horizontal range of the filter +* @param height {Number} the vertical range of the filter +*/ +PIXI.WebGLFilterManager.prototype.applyFilterPass = function(filter, filterArea, width, height) +{ + // use program + var gl = this.gl; + var shader = filter.shaders[gl.id]; + + if(!shader) + { + shader = new PIXI.PixiShader(gl); + + shader.fragmentSrc = filter.fragmentSrc; + shader.uniforms = filter.uniforms; + shader.init(); + + filter.shaders[gl.id] = shader; + } + + // set the shader + gl.useProgram(shader.program); + + gl.uniform2f(shader.projectionVector, width/2, -height/2); + gl.uniform2f(shader.offsetVector, 0,0); + + if(filter.uniforms.dimensions) + { + filter.uniforms.dimensions.value[0] = this.width;//width; + filter.uniforms.dimensions.value[1] = this.height;//height; + filter.uniforms.dimensions.value[2] = this.vertexArray[0]; + filter.uniforms.dimensions.value[3] = this.vertexArray[5];//filterArea.height; + } + + // console.log(this.uvArray ) + shader.syncUniforms(); + + gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); + gl.vertexAttribPointer(shader.aVertexPosition, 2, gl.FLOAT, false, 0, 0); + + gl.bindBuffer(gl.ARRAY_BUFFER, this.uvBuffer); + gl.vertexAttribPointer(shader.aTextureCoord, 2, gl.FLOAT, false, 0, 0); + + gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer); + gl.vertexAttribPointer(shader.colorAttribute, 2, gl.FLOAT, false, 0, 0); + + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); + + // draw the filter... + gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0 ); + + this.renderSession.drawCount++; +}; + +/** +* Initialises the shader buffers +* @method initShaderBuffers +*/ +PIXI.WebGLFilterManager.prototype.initShaderBuffers = function() +{ + var gl = this.gl; + + // create some buffers + this.vertexBuffer = gl.createBuffer(); + this.uvBuffer = gl.createBuffer(); + this.colorBuffer = gl.createBuffer(); + this.indexBuffer = gl.createBuffer(); + + + // bind and upload the vertexs.. + // keep a reference to the vertexFloatData.. + this.vertexArray = new Float32Array([0.0, 0.0, + 1.0, 0.0, + 0.0, 1.0, + 1.0, 1.0]); + + gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); + gl.bufferData( + gl.ARRAY_BUFFER, + this.vertexArray, + gl.STATIC_DRAW); + + + // bind and upload the uv buffer + this.uvArray = new Float32Array([0.0, 0.0, + 1.0, 0.0, + 0.0, 1.0, + 1.0, 1.0]); + + gl.bindBuffer(gl.ARRAY_BUFFER, this.uvBuffer); + gl.bufferData( + gl.ARRAY_BUFFER, + this.uvArray, + gl.STATIC_DRAW); + + this.colorArray = new Float32Array([1.0, 0xFFFFFF, + 1.0, 0xFFFFFF, + 1.0, 0xFFFFFF, + 1.0, 0xFFFFFF]); + + gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer); + gl.bufferData( + gl.ARRAY_BUFFER, + this.colorArray, + gl.STATIC_DRAW); + + // bind and upload the index + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); + gl.bufferData( + gl.ELEMENT_ARRAY_BUFFER, + new Uint16Array([0, 1, 2, 1, 3, 2]), + gl.STATIC_DRAW); +}; + +/** +* Destroys the filter and removes it from the filter stack +* @method destroy +*/ +PIXI.WebGLFilterManager.prototype.destroy = function() +{ + var gl = this.gl; + + this.filterStack = null; + + this.offsetX = 0; + this.offsetY = 0; + + // destroy textures + for (var i = 0; i < this.texturePool.length; i++) { + this.texturePool.destroy(); + } + + this.texturePool = null; + + //destroy buffers.. + gl.deleteBuffer(this.vertexBuffer); + gl.deleteBuffer(this.uvBuffer); + gl.deleteBuffer(this.colorBuffer); + gl.deleteBuffer(this.indexBuffer); +}; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +/** +* @class FilterTexture +* @constructor +* @param gl {WebGLContext} the current WebGL drawing context +* @param width {Number} the horizontal range of the filter +* @param height {Number} the vertical range of the filter +* @param scaleMode {Number} Should be one of the PIXI.scaleMode consts +* @private +*/ +PIXI.FilterTexture = function(gl, width, height, scaleMode) +{ + /** + * @property gl + * @type WebGLContext + */ + this.gl = gl; + + // next time to create a frame buffer and texture + this.frameBuffer = gl.createFramebuffer(); + this.texture = gl.createTexture(); + + scaleMode = scaleMode || PIXI.scaleModes.DEFAULT; + + gl.bindTexture(gl.TEXTURE_2D, this.texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, scaleMode === PIXI.scaleModes.LINEAR ? gl.LINEAR : gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, scaleMode === PIXI.scaleModes.LINEAR ? gl.LINEAR : gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer ); + + gl.bindFramebuffer(gl.FRAMEBUFFER, this.frameBuffer ); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture, 0); + + // required for masking a mask?? + this.renderBuffer = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, this.renderBuffer); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, this.renderBuffer); + + this.resize(width, height); +}; + + +/** +* Clears the filter texture +* @method clear +*/ +PIXI.FilterTexture.prototype.clear = function() +{ + var gl = this.gl; + + gl.clearColor(0,0,0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); +}; + +/** + * Resizes the texture to the specified width and height + * + * @method resize + * @param width {Number} the new width of the texture + * @param height {Number} the new height of the texture + */ +PIXI.FilterTexture.prototype.resize = function(width, height) +{ + if(this.width === width && this.height === height) return; + + this.width = width; + this.height = height; + + var gl = this.gl; + + gl.bindTexture(gl.TEXTURE_2D, this.texture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + + // update the stencil buffer width and height + gl.bindRenderbuffer(gl.RENDERBUFFER, this.renderBuffer); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, width, height); +}; + +/** +* Destroys the filter texture +* @method destroy +*/ +PIXI.FilterTexture.prototype.destroy = function() +{ + var gl = this.gl; + gl.deleteFramebuffer( this.frameBuffer ); + gl.deleteTexture( this.texture ); + + this.frameBuffer = null; + this.texture = null; +}; + +/** + * @author Mat Groves + * + * + */ +/** + * A set of functions used to handle masking + * + * @class CanvasMaskManager + */ +PIXI.CanvasMaskManager = function() +{ + +}; + +/** + * This method adds it to the current stack of masks + * + * @method pushMask + * @param maskData the maskData that will be pushed + * @param context {Context2D} the 2d drawing method of the canvas + */ +PIXI.CanvasMaskManager.prototype.pushMask = function(maskData, context) +{ + context.save(); + + var cacheAlpha = maskData.alpha; + var transform = maskData.worldTransform; + + context.setTransform(transform.a, transform.c, transform.b, transform.d, transform.tx, transform.ty); + + PIXI.CanvasGraphics.renderGraphicsMask(maskData, context); + + context.clip(); + + maskData.worldAlpha = cacheAlpha; +}; + +/** + * Restores the current drawing context to the state it was before the mask was applied + * + * @method popMask + * @param context {Context2D} the 2d drawing method of the canvas + */ +PIXI.CanvasMaskManager.prototype.popMask = function(context) +{ + context.restore(); +}; + +/** + * @author Mat Groves + * + * + */ + +/** + * @class CanvasTinter + * @constructor + * @static + */ +PIXI.CanvasTinter = function() +{ + /// this.textureCach +}; + +//PIXI.CanvasTinter.cachTint = true; + + +/** + * Basically this method just needs a sprite and a color and tints the sprite + * with the given color + * + * @method getTintedTexture + * @param sprite {Sprite} the sprite to tint + * @param color {Number} the color to use to tint the sprite with + */ +PIXI.CanvasTinter.getTintedTexture = function(sprite, color) +{ + + var texture = sprite.texture; + + color = PIXI.CanvasTinter.roundColor(color); + + var stringColor = "#" + ("00000" + ( color | 0).toString(16)).substr(-6); + + texture.tintCache = texture.tintCache || {}; + + if(texture.tintCache[stringColor]) return texture.tintCache[stringColor]; + + // clone texture.. + var canvas = PIXI.CanvasTinter.canvas || document.createElement("canvas"); + + //PIXI.CanvasTinter.tintWithPerPixel(texture, stringColor, canvas); + + + PIXI.CanvasTinter.tintMethod(texture, color, canvas); + + if(PIXI.CanvasTinter.convertTintToImage) + { + // is this better? + var tintImage = new Image(); + tintImage.src = canvas.toDataURL(); + + texture.tintCache[stringColor] = tintImage; + } + else + { + + texture.tintCache[stringColor] = canvas; + // if we are not converting the texture to an image then we need to lose the reference to the canvas + PIXI.CanvasTinter.canvas = null; + + } + + return canvas; +}; + +/** + * Tint a texture using the "multiply" operation + * @method tintWithMultiply + * @param texture {texture} the texture to tint + * @param color {Number} the color to use to tint the sprite with + * @param canvas {HTMLCanvasElement} the current canvas + */ +PIXI.CanvasTinter.tintWithMultiply = function(texture, color, canvas) +{ + var context = canvas.getContext( "2d" ); + + var frame = texture.frame; + + canvas.width = frame.width; + canvas.height = frame.height; + + context.fillStyle = "#" + ("00000" + ( color | 0).toString(16)).substr(-6); + + context.fillRect(0, 0, frame.width, frame.height); + + context.globalCompositeOperation = "multiply"; + + context.drawImage(texture.baseTexture.source, + frame.x, + frame.y, + frame.width, + frame.height, + 0, + 0, + frame.width, + frame.height); + + context.globalCompositeOperation = "destination-atop"; + + context.drawImage(texture.baseTexture.source, + frame.x, + frame.y, + frame.width, + frame.height, + 0, + 0, + frame.width, + frame.height); +}; + +/** + * Tint a texture using the "overlay" operation + * @method tintWithOverlay + * @param texture {texture} the texture to tint + * @param color {Number} the color to use to tint the sprite with + * @param canvas {HTMLCanvasElement} the current canvas + */ +PIXI.CanvasTinter.tintWithOverlay = function(texture, color, canvas) +{ + var context = canvas.getContext( "2d" ); + + var frame = texture.frame; + + canvas.width = frame.width; + canvas.height = frame.height; + + + + context.globalCompositeOperation = "copy"; + context.fillStyle = "#" + ("00000" + ( color | 0).toString(16)).substr(-6); + context.fillRect(0, 0, frame.width, frame.height); + + context.globalCompositeOperation = "destination-atop"; + context.drawImage(texture.baseTexture.source, + frame.x, + frame.y, + frame.width, + frame.height, + 0, + 0, + frame.width, + frame.height); + + + //context.globalCompositeOperation = "copy"; + +}; + +/** + * Tint a texture pixel per pixel + * @method tintPerPixel + * @param texture {texture} the texture to tint + * @param color {Number} the color to use to tint the sprite with + * @param canvas {HTMLCanvasElement} the current canvas + */ +PIXI.CanvasTinter.tintWithPerPixel = function(texture, color, canvas) +{ + var context = canvas.getContext( "2d" ); + + var frame = texture.frame; + + canvas.width = frame.width; + canvas.height = frame.height; + + context.globalCompositeOperation = "copy"; + context.drawImage(texture.baseTexture.source, + frame.x, + frame.y, + frame.width, + frame.height, + 0, + 0, + frame.width, + frame.height); + + var rgbValues = PIXI.hex2rgb(color); + var r = rgbValues[0], g = rgbValues[1], b = rgbValues[2]; + + var pixelData = context.getImageData(0, 0, frame.width, frame.height); + + var pixels = pixelData.data; + + for (var i = 0; i < pixels.length; i += 4) + { + pixels[i+0] *= r; + pixels[i+1] *= g; + pixels[i+2] *= b; + } + + context.putImageData(pixelData, 0, 0); +}; + +/** + * Rounds the specified color according to the PIXI.CanvasTinter.cacheStepsPerColorChannel + * @method roundColor + * @param color {number} the color to round, should be a hex color + */ +PIXI.CanvasTinter.roundColor = function(color) +{ + var step = PIXI.CanvasTinter.cacheStepsPerColorChannel; + + var rgbValues = PIXI.hex2rgb(color); + + rgbValues[0] = Math.min(255, (rgbValues[0] / step) * step); + rgbValues[1] = Math.min(255, (rgbValues[1] / step) * step); + rgbValues[2] = Math.min(255, (rgbValues[2] / step) * step); + + return PIXI.rgb2hex(rgbValues); +}; + +/** + * + * Number of steps which will be used as a cap when rounding colors + * + * @property cacheStepsPerColorChannel + * @type Number + */ +PIXI.CanvasTinter.cacheStepsPerColorChannel = 8; +/** + * + * Number of steps which will be used as a cap when rounding colors + * + * @property convertTintToImage + * @type Boolean + */ +PIXI.CanvasTinter.convertTintToImage = false; + +/** + * Whether or not the Canvas BlendModes are supported, consequently the ability to tint using the multiply method + * + * @property canUseMultiply + * @type Boolean + */ +PIXI.CanvasTinter.canUseMultiply = PIXI.canUseNewCanvasBlendModes(); + +PIXI.CanvasTinter.tintMethod = PIXI.CanvasTinter.canUseMultiply ? PIXI.CanvasTinter.tintWithMultiply : PIXI.CanvasTinter.tintWithPerPixel; + + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +/** + * the CanvasRenderer draws the stage and all its content onto a 2d canvas. This renderer should be used for browsers that do not support webGL. + * Dont forget to add the view to your DOM or you will not see anything :) + * + * @class CanvasRenderer + * @constructor + * @param width=800 {Number} the width of the canvas view + * @param height=600 {Number} the height of the canvas view + * @param [view] {HTMLCanvasElement} the canvas to use as a view, optional + * @param [transparent=false] {Boolean} the transparency of the render view, default false + */ +PIXI.CanvasRenderer = function(width, height, view, transparent) +{ + PIXI.defaultRenderer = PIXI.defaultRenderer || this; + + this.type = PIXI.CANVAS_RENDERER; + + /** + * This sets if the CanvasRenderer will clear the canvas or not before the new render pass. + * If the Stage is NOT transparent Pixi will use a canvas sized fillRect operation every frame to set the canvas background color. + * If the Stage is transparent Pixi will use clearRect to clear the canvas every frame. + * Disable this by setting this to false. For example if your game has a canvas filling background image you often don't need this set. + * + * @property clearBeforeRender + * @type Boolean + * @default + */ + this.clearBeforeRender = true; + + /** + * If true Pixi will Math.floor() x/y values when rendering, stopping pixel interpolation. + * Handy for crisp pixel art and speed on legacy devices. + * + * @property roundPixels + * @type Boolean + * @default + */ + this.roundPixels = false; + + /** + * Whether the render view is transparent + * + * @property transparent + * @type Boolean + */ + this.transparent = !!transparent; + + if(!PIXI.blendModesCanvas) + { + PIXI.blendModesCanvas = []; + + if(PIXI.canUseNewCanvasBlendModes()) + { + PIXI.blendModesCanvas[PIXI.blendModes.NORMAL] = "source-over"; + PIXI.blendModesCanvas[PIXI.blendModes.ADD] = "lighter"; //IS THIS OK??? + PIXI.blendModesCanvas[PIXI.blendModes.MULTIPLY] = "multiply"; + PIXI.blendModesCanvas[PIXI.blendModes.SCREEN] = "screen"; + PIXI.blendModesCanvas[PIXI.blendModes.OVERLAY] = "overlay"; + PIXI.blendModesCanvas[PIXI.blendModes.DARKEN] = "darken"; + PIXI.blendModesCanvas[PIXI.blendModes.LIGHTEN] = "lighten"; + PIXI.blendModesCanvas[PIXI.blendModes.COLOR_DODGE] = "color-dodge"; + PIXI.blendModesCanvas[PIXI.blendModes.COLOR_BURN] = "color-burn"; + PIXI.blendModesCanvas[PIXI.blendModes.HARD_LIGHT] = "hard-light"; + PIXI.blendModesCanvas[PIXI.blendModes.SOFT_LIGHT] = "soft-light"; + PIXI.blendModesCanvas[PIXI.blendModes.DIFFERENCE] = "difference"; + PIXI.blendModesCanvas[PIXI.blendModes.EXCLUSION] = "exclusion"; + PIXI.blendModesCanvas[PIXI.blendModes.HUE] = "hue"; + PIXI.blendModesCanvas[PIXI.blendModes.SATURATION] = "saturation"; + PIXI.blendModesCanvas[PIXI.blendModes.COLOR] = "color"; + PIXI.blendModesCanvas[PIXI.blendModes.LUMINOSITY] = "luminosity"; + } + else + { + // this means that the browser does not support the cool new blend modes in canvas "cough" ie "cough" + PIXI.blendModesCanvas[PIXI.blendModes.NORMAL] = "source-over"; + PIXI.blendModesCanvas[PIXI.blendModes.ADD] = "lighter"; //IS THIS OK??? + PIXI.blendModesCanvas[PIXI.blendModes.MULTIPLY] = "source-over"; + PIXI.blendModesCanvas[PIXI.blendModes.SCREEN] = "source-over"; + PIXI.blendModesCanvas[PIXI.blendModes.OVERLAY] = "source-over"; + PIXI.blendModesCanvas[PIXI.blendModes.DARKEN] = "source-over"; + PIXI.blendModesCanvas[PIXI.blendModes.LIGHTEN] = "source-over"; + PIXI.blendModesCanvas[PIXI.blendModes.COLOR_DODGE] = "source-over"; + PIXI.blendModesCanvas[PIXI.blendModes.COLOR_BURN] = "source-over"; + PIXI.blendModesCanvas[PIXI.blendModes.HARD_LIGHT] = "source-over"; + PIXI.blendModesCanvas[PIXI.blendModes.SOFT_LIGHT] = "source-over"; + PIXI.blendModesCanvas[PIXI.blendModes.DIFFERENCE] = "source-over"; + PIXI.blendModesCanvas[PIXI.blendModes.EXCLUSION] = "source-over"; + PIXI.blendModesCanvas[PIXI.blendModes.HUE] = "source-over"; + PIXI.blendModesCanvas[PIXI.blendModes.SATURATION] = "source-over"; + PIXI.blendModesCanvas[PIXI.blendModes.COLOR] = "source-over"; + PIXI.blendModesCanvas[PIXI.blendModes.LUMINOSITY] = "source-over"; + } + } + + /** + * The width of the canvas view + * + * @property width + * @type Number + * @default 800 + */ + this.width = width || 800; + + /** + * The height of the canvas view + * + * @property height + * @type Number + * @default 600 + */ + this.height = height || 600; + + /** + * The canvas element that everything is drawn to + * + * @property view + * @type HTMLCanvasElement + */ + this.view = view || document.createElement( "canvas" ); + + /** + * The canvas 2d context that everything is drawn with + * @property context + * @type HTMLCanvasElement 2d Context + */ + this.context = this.view.getContext( "2d", { alpha: this.transparent } ); + + this.refresh = true; + // hack to enable some hardware acceleration! + //this.view.style["transform"] = "translatez(0)"; + + this.view.width = this.width; + this.view.height = this.height; + this.count = 0; + + /** + * Instance of a PIXI.CanvasMaskManager, handles masking when using the canvas renderer + * @property CanvasMaskManager + * @type CanvasMaskManager + */ + this.maskManager = new PIXI.CanvasMaskManager(); + + /** + * The render session is just a bunch of parameter used for rendering + * @property renderSession + * @type Object + */ + this.renderSession = { + context: this.context, + maskManager: this.maskManager, + scaleMode: null, + smoothProperty: null + }; + + if("imageSmoothingEnabled" in this.context) + this.renderSession.smoothProperty = "imageSmoothingEnabled"; + else if("webkitImageSmoothingEnabled" in this.context) + this.renderSession.smoothProperty = "webkitImageSmoothingEnabled"; + else if("mozImageSmoothingEnabled" in this.context) + this.renderSession.smoothProperty = "mozImageSmoothingEnabled"; + else if("oImageSmoothingEnabled" in this.context) + this.renderSession.smoothProperty = "oImageSmoothingEnabled"; +}; + +// constructor +PIXI.CanvasRenderer.prototype.constructor = PIXI.CanvasRenderer; + +/** + * Renders the stage to its canvas view + * + * @method render + * @param stage {Stage} the Stage element to be rendered + */ +PIXI.CanvasRenderer.prototype.render = function(stage) +{ + // update textures if need be + PIXI.texturesToUpdate.length = 0; + PIXI.texturesToDestroy.length = 0; + + stage.updateTransform(); + + this.context.setTransform(1,0,0,1,0,0); + this.context.globalAlpha = 1; + + if (!this.transparent && this.clearBeforeRender) + { + this.context.fillStyle = stage.backgroundColorString; + this.context.fillRect(0, 0, this.width, this.height); + } + else if (this.transparent && this.clearBeforeRender) + { + this.context.clearRect(0, 0, this.width, this.height); + } + + this.renderDisplayObject(stage); + + // run interaction! + if(stage.interactive) + { + //need to add some events! + if(!stage._interactiveEventsAdded) + { + stage._interactiveEventsAdded = true; + stage.interactionManager.setTarget(this); + } + } + + // remove frame updates.. + if(PIXI.Texture.frameUpdates.length > 0) + { + PIXI.Texture.frameUpdates.length = 0; + } +}; + +/** + * Resizes the canvas view to the specified width and height + * + * @method resize + * @param width {Number} the new width of the canvas view + * @param height {Number} the new height of the canvas view + */ +PIXI.CanvasRenderer.prototype.resize = function(width, height) +{ + this.width = width; + this.height = height; + + this.view.width = width; + this.view.height = height; +}; + +/** + * Renders a display object + * + * @method renderDisplayObject + * @param displayObject {DisplayObject} The displayObject to render + * @param context {Context2D} the context 2d method of the canvas + * @private + */ +PIXI.CanvasRenderer.prototype.renderDisplayObject = function(displayObject, context) +{ + // no longer recursive! + //var transform; + //var context = this.context; + + this.renderSession.context = context || this.context; + displayObject._renderCanvas(this.renderSession); +}; + +/** + * Renders a flat strip + * + * @method renderStripFlat + * @param strip {Strip} The Strip to render + * @private + */ +PIXI.CanvasRenderer.prototype.renderStripFlat = function(strip) +{ + var context = this.context; + var verticies = strip.verticies; + + var length = verticies.length/2; + this.count++; + + context.beginPath(); + for (var i=1; i < length-2; i++) + { + // draw some triangles! + var index = i*2; + + var x0 = verticies[index], x1 = verticies[index+2], x2 = verticies[index+4]; + var y0 = verticies[index+1], y1 = verticies[index+3], y2 = verticies[index+5]; + + context.moveTo(x0, y0); + context.lineTo(x1, y1); + context.lineTo(x2, y2); + } + + context.fillStyle = "#FF0000"; + context.fill(); + context.closePath(); +}; + +/** + * Renders a strip + * + * @method renderStrip + * @param strip {Strip} The Strip to render + * @private + */ +PIXI.CanvasRenderer.prototype.renderStrip = function(strip) +{ + var context = this.context; + + // draw triangles!! + var verticies = strip.verticies; + var uvs = strip.uvs; + + var length = verticies.length/2; + this.count++; + + for (var i = 1; i < length-2; i++) + { + // draw some triangles! + var index = i*2; + + var x0 = verticies[index], x1 = verticies[index+2], x2 = verticies[index+4]; + var y0 = verticies[index+1], y1 = verticies[index+3], y2 = verticies[index+5]; + + var u0 = uvs[index] * strip.texture.width, u1 = uvs[index+2] * strip.texture.width, u2 = uvs[index+4]* strip.texture.width; + var v0 = uvs[index+1]* strip.texture.height, v1 = uvs[index+3] * strip.texture.height, v2 = uvs[index+5]* strip.texture.height; + + context.save(); + context.beginPath(); + context.moveTo(x0, y0); + context.lineTo(x1, y1); + context.lineTo(x2, y2); + context.closePath(); + + context.clip(); + + // Compute matrix transform + var delta = u0*v1 + v0*u2 + u1*v2 - v1*u2 - v0*u1 - u0*v2; + var deltaA = x0*v1 + v0*x2 + x1*v2 - v1*x2 - v0*x1 - x0*v2; + var deltaB = u0*x1 + x0*u2 + u1*x2 - x1*u2 - x0*u1 - u0*x2; + var deltaC = u0*v1*x2 + v0*x1*u2 + x0*u1*v2 - x0*v1*u2 - v0*u1*x2 - u0*x1*v2; + var deltaD = y0*v1 + v0*y2 + y1*v2 - v1*y2 - v0*y1 - y0*v2; + var deltaE = u0*y1 + y0*u2 + u1*y2 - y1*u2 - y0*u1 - u0*y2; + var deltaF = u0*v1*y2 + v0*y1*u2 + y0*u1*v2 - y0*v1*u2 - v0*u1*y2 - u0*y1*v2; + + context.transform(deltaA / delta, deltaD / delta, + deltaB / delta, deltaE / delta, + deltaC / delta, deltaF / delta); + + context.drawImage(strip.texture.baseTexture.source, 0, 0); + context.restore(); + } +}; + +/** + * Creates a Canvas element of the given size + * + * @method CanvasBuffer + * @param width {Number} the width for the newly created canvas + * @param height {Number} the height for the newly created canvas + * @static + * @private + */ +PIXI.CanvasBuffer = function(width, height) +{ + this.width = width; + this.height = height; + + this.canvas = document.createElement( "canvas" ); + this.context = this.canvas.getContext( "2d" ); + + this.canvas.width = width; + this.canvas.height = height; +}; + +/** + * Clears the canvas that was created by the CanvasBuffer class + * + * @method clear + * @private + */ +PIXI.CanvasBuffer.prototype.clear = function() +{ + this.context.clearRect(0,0, this.width, this.height); +}; + +/** + * Resizes the canvas that was created by the CanvasBuffer class to the specified width and height + * + * @method resize + * @param width {Number} the new width of the canvas + * @param height {Number} the new height of the canvas + * @private + */ + +PIXI.CanvasBuffer.prototype.resize = function(width, height) +{ + this.width = this.canvas.width = width; + this.height = this.canvas.height = height; +}; + + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + + +/** + * A set of functions used by the canvas renderer to draw the primitive graphics data + * + * @class CanvasGraphics + */ +PIXI.CanvasGraphics = function() +{ + +}; + + +/* + * Renders the graphics object + * + * @static + * @private + * @method renderGraphics + * @param graphics {Graphics} the actual graphics object to render + * @param context {Context2D} the 2d drawing method of the canvas + */ +PIXI.CanvasGraphics.renderGraphics = function(graphics, context) +{ + var worldAlpha = graphics.worldAlpha; + var color = ''; + + for (var i = 0; i < graphics.graphicsData.length; i++) + { + var data = graphics.graphicsData[i]; + var points = data.points; + + context.strokeStyle = color = '#' + ('00000' + ( data.lineColor | 0).toString(16)).substr(-6); + + context.lineWidth = data.lineWidth; + + if(data.type === PIXI.Graphics.POLY) + { + context.beginPath(); + + context.moveTo(points[0], points[1]); + + for (var j=1; j < points.length/2; j++) + { + context.lineTo(points[j * 2], points[j * 2 + 1]); + } + + // if the first and last point are the same close the path - much neater :) + if(points[0] === points[points.length-2] && points[1] === points[points.length-1]) + { + context.closePath(); + } + + if(data.fill) + { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = color = '#' + ('00000' + ( data.fillColor | 0).toString(16)).substr(-6); + context.fill(); + } + if(data.lineWidth) + { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.stroke(); + } + } + else if(data.type === PIXI.Graphics.RECT) + { + + if(data.fillColor || data.fillColor === 0) + { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = color = '#' + ('00000' + ( data.fillColor | 0).toString(16)).substr(-6); + context.fillRect(points[0], points[1], points[2], points[3]); + + } + if(data.lineWidth) + { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeRect(points[0], points[1], points[2], points[3]); + } + + } + else if(data.type === PIXI.Graphics.CIRC) + { + // TODO - need to be Undefined! + context.beginPath(); + context.arc(points[0], points[1], points[2],0,2*Math.PI); + context.closePath(); + + if(data.fill) + { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = color = '#' + ('00000' + ( data.fillColor | 0).toString(16)).substr(-6); + context.fill(); + } + if(data.lineWidth) + { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.stroke(); + } + } + else if(data.type === PIXI.Graphics.ELIP) + { + + // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + + var ellipseData = data.points; + + var w = ellipseData[2] * 2; + var h = ellipseData[3] * 2; + + var x = ellipseData[0] - w/2; + var y = ellipseData[1] - h/2; + + context.beginPath(); + + var kappa = 0.5522848, + ox = (w / 2) * kappa, // control point offset horizontal + oy = (h / 2) * kappa, // control point offset vertical + xe = x + w, // x-end + ye = y + h, // y-end + xm = x + w / 2, // x-middle + ym = y + h / 2; // y-middle + + context.moveTo(x, ym); + context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + + context.closePath(); + + if(data.fill) + { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = color = '#' + ('00000' + ( data.fillColor | 0).toString(16)).substr(-6); + context.fill(); + } + if(data.lineWidth) + { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.stroke(); + } + } + } +}; + +/* + * Renders a graphics mask + * + * @static + * @private + * @method renderGraphicsMask + * @param graphics {Graphics} the graphics which will be used as a mask + * @param context {Context2D} the context 2d method of the canvas + */ +PIXI.CanvasGraphics.renderGraphicsMask = function(graphics, context) +{ + var len = graphics.graphicsData.length; + + if(len === 0) return; + + if(len > 1) + { + len = 1; + window.console.log('Pixi.js warning: masks in canvas can only mask using the first path in the graphics object'); + } + + for (var i = 0; i < 1; i++) + { + var data = graphics.graphicsData[i]; + var points = data.points; + + if(data.type === PIXI.Graphics.POLY) + { + context.beginPath(); + context.moveTo(points[0], points[1]); + + for (var j=1; j < points.length/2; j++) + { + context.lineTo(points[j * 2], points[j * 2 + 1]); + } + + // if the first and last point are the same close the path - much neater :) + if(points[0] === points[points.length-2] && points[1] === points[points.length-1]) + { + context.closePath(); + } + + } + else if(data.type === PIXI.Graphics.RECT) + { + context.beginPath(); + context.rect(points[0], points[1], points[2], points[3]); + context.closePath(); + } + else if(data.type === PIXI.Graphics.CIRC) + { + // TODO - need to be Undefined! + context.beginPath(); + context.arc(points[0], points[1], points[2],0,2*Math.PI); + context.closePath(); + } + else if(data.type === PIXI.Graphics.ELIP) + { + + // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + var ellipseData = data.points; + + var w = ellipseData[2] * 2; + var h = ellipseData[3] * 2; + + var x = ellipseData[0] - w/2; + var y = ellipseData[1] - h/2; + + context.beginPath(); + + var kappa = 0.5522848, + ox = (w / 2) * kappa, // control point offset horizontal + oy = (h / 2) * kappa, // control point offset vertical + xe = x + w, // x-end + ye = y + h, // y-end + xm = x + w / 2, // x-middle + ym = y + h / 2; // y-middle + + context.moveTo(x, ym); + context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + context.closePath(); + } + } +}; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + + +/** + * The Graphics class contains a set of methods that you can use to create primitive shapes and lines. + * It is important to know that with the webGL renderer only simple polygons can be filled at this stage + * Complex polygons will not be filled. Heres an example of a complex polygon: http://www.goodboydigital.com/wp-content/uploads/2013/06/complexPolygon.png + * + * @class Graphics + * @extends DisplayObjectContainer + * @constructor + */ +PIXI.Graphics = function() +{ + PIXI.DisplayObjectContainer.call( this ); + + this.renderable = true; + + /** + * The alpha of the fill of this graphics object + * + * @property fillAlpha + * @type Number + */ + this.fillAlpha = 1; + + /** + * The width of any lines drawn + * + * @property lineWidth + * @type Number + */ + this.lineWidth = 0; + + /** + * The color of any lines drawn + * + * @property lineColor + * @type String + */ + this.lineColor = "black"; + + /** + * Graphics data + * + * @property graphicsData + * @type Array + * @private + */ + this.graphicsData = []; + + + /** + * The tint applied to the graphic shape. This is a hex value + * + * @property tint + * @type Number + * @default 0xFFFFFF + */ + this.tint = 0xFFFFFF;// * Math.random(); + + /** + * The blend mode to be applied to the graphic shape + * + * @property blendMode + * @type Number + * @default PIXI.blendModes.NORMAL; + */ + this.blendMode = PIXI.blendModes.NORMAL; + + /** + * Current path + * + * @property currentPath + * @type Object + * @private + */ + this.currentPath = {points:[]}; + + /** + * Array containing some WebGL-related properties used by the WebGL renderer + * + * @property _webGL + * @type Array + * @private + */ + this._webGL = []; + + /** + * Whether this shape is being used as a mask + * + * @property isMask + * @type isMask + */ + this.isMask = false; + + /** + * The bounds of the graphic shape as rectangle object + * + * @property bounds + * @type Rectangle + */ + this.bounds = null; + + /** + * the bounds' padding used for bounds calculation + * + * @property boundsPadding + * @type Number + */ + this.boundsPadding = 10; +}; + +// constructor +PIXI.Graphics.prototype = Object.create( PIXI.DisplayObjectContainer.prototype ); +PIXI.Graphics.prototype.constructor = PIXI.Graphics; + +/** + * If cacheAsBitmap is true the graphics object will then be rendered as if it was a sprite. + * This is useful if your graphics element does not change often as it will speed up the rendering of the object + * It is also usful as the graphics object will always be antialiased because it will be rendered using canvas + * Not recommended if you are constanly redrawing the graphics element. + * + * @property cacheAsBitmap + * @default false + * @type Boolean + * @private + */ +Object.defineProperty(PIXI.Graphics.prototype, "cacheAsBitmap", { + get: function() { + return this._cacheAsBitmap; + }, + set: function(value) { + this._cacheAsBitmap = value; + + if(this._cacheAsBitmap) + { + this._generateCachedSprite(); + } + else + { + this.destroyCachedSprite(); + this.dirty = true; + } + + } +}); + + +/** + * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() method or the drawCircle() method. + * + * @method lineStyle + * @param lineWidth {Number} width of the line to draw, will update the object's stored style + * @param color {Number} color of the line to draw, will update the object's stored style + * @param alpha {Number} alpha of the line to draw, will update the object's stored style + */ +PIXI.Graphics.prototype.lineStyle = function(lineWidth, color, alpha) +{ + if (!this.currentPath.points.length) this.graphicsData.pop(); + + this.lineWidth = lineWidth || 0; + this.lineColor = color || 0; + this.lineAlpha = (arguments.length < 3) ? 1 : alpha; + + this.currentPath = {lineWidth:this.lineWidth, lineColor:this.lineColor, lineAlpha:this.lineAlpha, + fillColor:this.fillColor, fillAlpha:this.fillAlpha, fill:this.filling, points:[], type:PIXI.Graphics.POLY}; + + this.graphicsData.push(this.currentPath); + + return this; +}; + +/** + * Moves the current drawing position to (x, y). + * + * @method moveTo + * @param x {Number} the X coordinate to move to + * @param y {Number} the Y coordinate to move to + */ +PIXI.Graphics.prototype.moveTo = function(x, y) +{ + if (!this.currentPath.points.length) this.graphicsData.pop(); + + this.currentPath = this.currentPath = {lineWidth:this.lineWidth, lineColor:this.lineColor, lineAlpha:this.lineAlpha, + fillColor:this.fillColor, fillAlpha:this.fillAlpha, fill:this.filling, points:[], type:PIXI.Graphics.POLY}; + + this.currentPath.points.push(x, y); + + this.graphicsData.push(this.currentPath); + + return this; +}; + +/** + * Draws a line using the current line style from the current drawing position to (x, y); + * the current drawing position is then set to (x, y). + * + * @method lineTo + * @param x {Number} the X coordinate to draw to + * @param y {Number} the Y coordinate to draw to + */ +PIXI.Graphics.prototype.lineTo = function(x, y) +{ + this.currentPath.points.push(x, y); + this.dirty = true; + + return this; +}; + +/** + * Specifies a simple one-color fill that subsequent calls to other Graphics methods + * (such as lineTo() or drawCircle()) use when drawing. + * + * @method beginFill + * @param color {Number} the color of the fill + * @param alpha {Number} the alpha of the fill + */ +PIXI.Graphics.prototype.beginFill = function(color, alpha) +{ + + this.filling = true; + this.fillColor = color || 0; + this.fillAlpha = (arguments.length < 2) ? 1 : alpha; + + return this; +}; + +/** + * Applies a fill to the lines and shapes that were added since the last call to the beginFill() method. + * + * @method endFill + */ +PIXI.Graphics.prototype.endFill = function() +{ + this.filling = false; + this.fillColor = null; + this.fillAlpha = 1; + + return this; +}; + +/** + * @method drawRect + * + * @param x {Number} The X coord of the top-left of the rectangle + * @param y {Number} The Y coord of the top-left of the rectangle + * @param width {Number} The width of the rectangle + * @param height {Number} The height of the rectangle + */ +PIXI.Graphics.prototype.drawRect = function( x, y, width, height ) +{ + if (!this.currentPath.points.length) this.graphicsData.pop(); + + this.currentPath = {lineWidth:this.lineWidth, lineColor:this.lineColor, lineAlpha:this.lineAlpha, + fillColor:this.fillColor, fillAlpha:this.fillAlpha, fill:this.filling, + points:[x, y, width, height], type:PIXI.Graphics.RECT}; + + this.graphicsData.push(this.currentPath); + this.dirty = true; + + return this; +}; + +/** + * Draws a circle. + * + * @method drawCircle + * @param x {Number} The X coordinate of the center of the circle + * @param y {Number} The Y coordinate of the center of the circle + * @param radius {Number} The radius of the circle + */ +PIXI.Graphics.prototype.drawCircle = function( x, y, radius) +{ + + if (!this.currentPath.points.length) this.graphicsData.pop(); + + this.currentPath = {lineWidth:this.lineWidth, lineColor:this.lineColor, lineAlpha:this.lineAlpha, + fillColor:this.fillColor, fillAlpha:this.fillAlpha, fill:this.filling, + points:[x, y, radius, radius], type:PIXI.Graphics.CIRC}; + + this.graphicsData.push(this.currentPath); + this.dirty = true; + + return this; +}; + +/** + * Draws an ellipse. + * + * @method drawEllipse + * @param x {Number} The X coordinate of the upper-left corner of the framing rectangle of this ellipse + * @param y {Number} The Y coordinate of the upper-left corner of the framing rectangle of this ellipse + * @param width {Number} The width of the ellipse + * @param height {Number} The height of the ellipse + */ +PIXI.Graphics.prototype.drawEllipse = function( x, y, width, height) +{ + + if (!this.currentPath.points.length) this.graphicsData.pop(); + + this.currentPath = {lineWidth:this.lineWidth, lineColor:this.lineColor, lineAlpha:this.lineAlpha, + fillColor:this.fillColor, fillAlpha:this.fillAlpha, fill:this.filling, + points:[x, y, width, height], type:PIXI.Graphics.ELIP}; + + this.graphicsData.push(this.currentPath); + this.dirty = true; + + return this; +}; + +/** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @method clear + */ +PIXI.Graphics.prototype.clear = function() +{ + this.lineWidth = 0; + this.filling = false; + + this.dirty = true; + this.clearDirty = true; + this.graphicsData = []; + + this.bounds = null; //new PIXI.Rectangle(); + + return this; +}; + +/** + * Useful function that returns a texture of the graphics object that can then be used to create sprites + * This can be quite useful if your geometry is complicated and needs to be reused multiple times. + * + * @method generateTexture + * @return {Texture} a texture of the graphics object + */ +PIXI.Graphics.prototype.generateTexture = function() +{ + var bounds = this.getBounds(); + + var canvasBuffer = new PIXI.CanvasBuffer(bounds.width, bounds.height); + var texture = PIXI.Texture.fromCanvas(canvasBuffer.canvas); + + canvasBuffer.context.translate(-bounds.x,-bounds.y); + + PIXI.CanvasGraphics.renderGraphics(this, canvasBuffer.context); + + return texture; +}; + +/** +* Renders the object using the WebGL renderer +* +* @method _renderWebGL +* @param renderSession {RenderSession} +* @private +*/ +PIXI.Graphics.prototype._renderWebGL = function(renderSession) +{ + // if the sprite is not visible or the alpha is 0 then no need to render this element + if(this.visible === false || this.alpha === 0 || this.isMask === true)return; + + if(this._cacheAsBitmap) + { + + if(this.dirty) + { + this._generateCachedSprite(); + // we will also need to update the texture on the gpu too! + PIXI.updateWebGLTexture(this._cachedSprite.texture.baseTexture, renderSession.gl); + + this.dirty = false; + } + + this._cachedSprite.alpha = this.alpha; + PIXI.Sprite.prototype._renderWebGL.call(this._cachedSprite, renderSession); + + return; + } + else + { + renderSession.spriteBatch.stop(); + + if(this._mask)renderSession.maskManager.pushMask(this.mask, renderSession); + if(this._filters)renderSession.filterManager.pushFilter(this._filterBlock); + + // check blend mode + if(this.blendMode !== renderSession.spriteBatch.currentBlendMode) + { + renderSession.spriteBatch.currentBlendMode = this.blendMode; + var blendModeWebGL = PIXI.blendModesWebGL[renderSession.spriteBatch.currentBlendMode]; + renderSession.spriteBatch.gl.blendFunc(blendModeWebGL[0], blendModeWebGL[1]); + } + + PIXI.WebGLGraphics.renderGraphics(this, renderSession); + + // only render if it has children! + if(this.children.length) + { + renderSession.spriteBatch.start(); + + // simple render children! + for(var i=0, j=this.children.length; i maxX ? x2 : maxX; + maxX = x3 > maxX ? x3 : maxX; + maxX = x4 > maxX ? x4 : maxX; + + maxY = y2 > maxY ? y2 : maxY; + maxY = y3 > maxY ? y3 : maxY; + maxY = y4 > maxY ? y4 : maxY; + + var bounds = this._bounds; + + bounds.x = minX; + bounds.width = maxX - minX; + + bounds.y = minY; + bounds.height = maxY - minY; + + return bounds; +}; + +/** + * Update the bounds of the object + * + * @method updateBounds + */ +PIXI.Graphics.prototype.updateBounds = function() +{ + + var minX = Infinity; + var maxX = -Infinity; + + var minY = Infinity; + var maxY = -Infinity; + + var points, x, y, w, h; + + for (var i = 0; i < this.graphicsData.length; i++) { + var data = this.graphicsData[i]; + var type = data.type; + var lineWidth = data.lineWidth; + + points = data.points; + + if(type === PIXI.Graphics.RECT) + { + x = points[0] - lineWidth/2; + y = points[1] - lineWidth/2; + w = points[2] + lineWidth; + h = points[3] + lineWidth; + + minX = x < minX ? x : minX; + maxX = x + w > maxX ? x + w : maxX; + + minY = y < minY ? x : minY; + maxY = y + h > maxY ? y + h : maxY; + } + else if(type === PIXI.Graphics.CIRC || type === PIXI.Graphics.ELIP) + { + x = points[0]; + y = points[1]; + w = points[2] + lineWidth/2; + h = points[3] + lineWidth/2; + + minX = x - w < minX ? x - w : minX; + maxX = x + w > maxX ? x + w : maxX; + + minY = y - h < minY ? y - h : minY; + maxY = y + h > maxY ? y + h : maxY; + } + else + { + // POLY + for (var j = 0; j < points.length; j+=2) + { + + x = points[j]; + y = points[j+1]; + minX = x-lineWidth < minX ? x-lineWidth : minX; + maxX = x+lineWidth > maxX ? x+lineWidth : maxX; + + minY = y-lineWidth < minY ? y-lineWidth : minY; + maxY = y+lineWidth > maxY ? y+lineWidth : maxY; + } + } + } + + var padding = this.boundsPadding; + this.bounds = new PIXI.Rectangle(minX - padding, minY - padding, (maxX - minX) + padding * 2, (maxY - minY) + padding * 2); +}; + + +/** + * Generates the cached sprite when the sprite has cacheAsBitmap = true + * + * @method _generateCachedSprite + * @private + */ +PIXI.Graphics.prototype._generateCachedSprite = function() +{ + var bounds = this.getLocalBounds(); + + if(!this._cachedSprite) + { + var canvasBuffer = new PIXI.CanvasBuffer(bounds.width, bounds.height); + var texture = PIXI.Texture.fromCanvas(canvasBuffer.canvas); + + this._cachedSprite = new PIXI.Sprite(texture); + this._cachedSprite.buffer = canvasBuffer; + + this._cachedSprite.worldTransform = this.worldTransform; + } + else + { + this._cachedSprite.buffer.resize(bounds.width, bounds.height); + } + + // leverage the anchor to account for the offset of the element + this._cachedSprite.anchor.x = -( bounds.x / bounds.width ); + this._cachedSprite.anchor.y = -( bounds.y / bounds.height ); + + // this._cachedSprite.buffer.context.save(); + this._cachedSprite.buffer.context.translate(-bounds.x,-bounds.y); + + PIXI.CanvasGraphics.renderGraphics(this, this._cachedSprite.buffer.context); + this._cachedSprite.alpha = this.alpha; + + // this._cachedSprite.buffer.context.restore(); +}; + +PIXI.Graphics.prototype.destroyCachedSprite = function() +{ + this._cachedSprite.texture.destroy(true); + + // let the gc collect the unused sprite + // TODO could be object pooled! + this._cachedSprite = null; +}; + + +// SOME TYPES: +PIXI.Graphics.POLY = 0; +PIXI.Graphics.RECT = 1; +PIXI.Graphics.CIRC = 2; +PIXI.Graphics.ELIP = 3; + +/** + * @author Mat Groves http://matgroves.com/ + */ + + /** + * + * @class Strip + * @extends DisplayObjectContainer + * @constructor + * @param texture {Texture} The texture to use + * @param width {Number} the width + * @param height {Number} the height + * + */ +PIXI.Strip = function(texture, width, height) +{ + PIXI.Sprite.call( this, texture ); + this.width =width; + this.height = height; + this.texture = texture; + this.blendMode = PIXI.blendModes.NORMAL; + + try + { + this.uvs = new Float32Array([0, 1, + 1, 1, + 1, 0, 0,1]); + + this.verticies = new Float32Array([0, 0, + 0,0, + 0,0, 0, + 0, 0]); + + this.colors = new Float32Array([1, 1, 1, 1]); + + this.indices = new Uint16Array([0, 1, 2, 3]); + } + catch(error) + { + this.uvs = [0, 1, + 1, 1, + 1, 0, 0,1]; + + this.verticies = [0, 0, + 0,0, + 0,0, 0, + 0, 0]; + + this.colors = [1, 1, 1, 1]; + + this.indices = [0, 1, 2, 3]; + } + + + /* + this.uvs = new Float32Array() + this.verticies = new Float32Array() + this.colors = new Float32Array() + this.indices = new Uint16Array() + */ + + // this.width = width; + // this.height = height; + + // load the texture! + + if(texture.baseTexture.hasLoaded) + { + this.width = this.texture.frame.width; + this.height = this.texture.frame.height; + this.updateFrame = true; + } + else + { + this.onTextureUpdateBind = this.onTextureUpdate.bind(this); + this.texture.addEventListener( 'update', this.onTextureUpdateBind ); + } + + this.renderable = true; +}; + +// constructor +PIXI.Strip.prototype = Object.create(PIXI.Sprite.prototype); +PIXI.Strip.prototype.constructor = PIXI.Strip; + +/* + * Sets the texture that the Strip will use + * + * @method setTexture + * @param texture {Texture} the texture that will be used + * @private + */ + +/* +PIXI.Strip.prototype.setTexture = function(texture) +{ + //TODO SET THE TEXTURES + //TODO VISIBILITY + + // stop current texture + this.texture = texture; + this.width = texture.frame.width; + this.height = texture.frame.height; + this.updateFrame = true; +}; +*/ + +/** + * When the texture is updated, this event will fire to update the scale and frame + * + * @method onTextureUpdate + * @param event + * @private + */ + +PIXI.Strip.prototype.onTextureUpdate = function() +{ + this.updateFrame = true; +}; +/* @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +/** + * + * @class Rope + * @constructor + * @param texture {Texture} The texture to use + * @param points {Array} + * + */ +PIXI.Rope = function(texture, points) +{ + PIXI.Strip.call( this, texture ); + this.points = points; + + try + { + this.verticies = new Float32Array(points.length * 4); + this.uvs = new Float32Array(points.length * 4); + this.colors = new Float32Array(points.length * 2); + this.indices = new Uint16Array(points.length * 2); + } + catch(error) + { + this.verticies = new Array(points.length * 4); + this.uvs = new Array(points.length * 4); + this.colors = new Array(points.length * 2); + this.indices = new Array(points.length * 2); + } + + this.refresh(); +}; + + +// constructor +PIXI.Rope.prototype = Object.create( PIXI.Strip.prototype ); +PIXI.Rope.prototype.constructor = PIXI.Rope; + +/* + * Refreshes + * + * @method refresh + */ +PIXI.Rope.prototype.refresh = function() +{ + var points = this.points; + if(points.length < 1) return; + + var uvs = this.uvs; + + var lastPoint = points[0]; + var indices = this.indices; + var colors = this.colors; + + this.count-=0.2; + + + uvs[0] = 0; + uvs[1] = 1; + uvs[2] = 0; + uvs[3] = 1; + + colors[0] = 1; + colors[1] = 1; + + indices[0] = 0; + indices[1] = 1; + + var total = points.length, + point, index, amount; + + for (var i = 1; i < total; i++) + { + + point = points[i]; + index = i * 4; + // time to do some smart drawing! + amount = i / (total-1); + + if(i%2) + { + uvs[index] = amount; + uvs[index+1] = 0; + + uvs[index+2] = amount; + uvs[index+3] = 1; + + } + else + { + uvs[index] = amount; + uvs[index+1] = 0; + + uvs[index+2] = amount; + uvs[index+3] = 1; + } + + index = i * 2; + colors[index] = 1; + colors[index+1] = 1; + + index = i * 2; + indices[index] = index; + indices[index + 1] = index + 1; + + lastPoint = point; + } +}; + +/* + * Updates the object transform for rendering + * + * @method updateTransform + * @private + */ +PIXI.Rope.prototype.updateTransform = function() +{ + + var points = this.points; + if(points.length < 1)return; + + var lastPoint = points[0]; + var nextPoint; + var perp = {x:0, y:0}; + + this.count-=0.2; + + var verticies = this.verticies; + verticies[0] = lastPoint.x + perp.x; + verticies[1] = lastPoint.y + perp.y; //+ 200 + verticies[2] = lastPoint.x - perp.x; + verticies[3] = lastPoint.y - perp.y;//+200 + // time to do some smart drawing! + + var total = points.length, + point, index, ratio, perpLength, num; + + for (var i = 1; i < total; i++) + { + point = points[i]; + index = i * 4; + + if(i < points.length-1) + { + nextPoint = points[i+1]; + } + else + { + nextPoint = point; + } + + perp.y = -(nextPoint.x - lastPoint.x); + perp.x = nextPoint.y - lastPoint.y; + + ratio = (1 - (i / (total-1))) * 10; + + if(ratio > 1) ratio = 1; + + perpLength = Math.sqrt(perp.x * perp.x + perp.y * perp.y); + num = this.texture.height / 2; //(20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + perp.x /= perpLength; + perp.y /= perpLength; + + perp.x *= num; + perp.y *= num; + + verticies[index] = point.x + perp.x; + verticies[index+1] = point.y + perp.y; + verticies[index+2] = point.x - perp.x; + verticies[index+3] = point.y - perp.y; + + lastPoint = point; + } + + PIXI.DisplayObjectContainer.prototype.updateTransform.call( this ); +}; +/* + * Sets the texture that the Rope will use + * + * @method setTexture + * @param texture {Texture} the texture that will be used + */ +PIXI.Rope.prototype.setTexture = function(texture) +{ + // stop current texture + this.texture = texture; + this.updateFrame = true; +}; + +/** + * @author Mat Groves http://matgroves.com/ + */ + +/** + * A tiling sprite is a fast way of rendering a tiling image + * + * @class TilingSprite + * @extends Sprite + * @constructor + * @param texture {Texture} the texture of the tiling sprite + * @param width {Number} the width of the tiling sprite + * @param height {Number} the height of the tiling sprite + */ +PIXI.TilingSprite = function(texture, width, height) +{ + PIXI.Sprite.call( this, texture); + + /** + * The with of the tiling sprite + * + * @property width + * @type Number + */ + this.width = width || 100; + + /** + * The height of the tiling sprite + * + * @property height + * @type Number + */ + this.height = height || 100; + + /** + * The scaling of the image that is being tiled + * + * @property tileScale + * @type Point + */ + this.tileScale = new PIXI.Point(1,1); + + /** + * A point that represents the scale of the texture object + * + * @property tileScaleOffset + * @type Point + */ + this.tileScaleOffset = new PIXI.Point(1,1); + + /** + * The offset position of the image that is being tiled + * + * @property tilePosition + * @type Point + */ + this.tilePosition = new PIXI.Point(0,0); + + + /** + * Whether this sprite is renderable or not + * + * @property renderable + * @type Boolean + * @default true + */ + this.renderable = true; + + /** + * The tint applied to the sprite. This is a hex value + * + * @property tint + * @type Number + * @default 0xFFFFFF + */ + this.tint = 0xFFFFFF; + + /** + * The blend mode to be applied to the sprite + * + * @property blendMode + * @type Number + * @default PIXI.blendModes.NORMAL; + */ + this.blendMode = PIXI.blendModes.NORMAL; +}; + +// constructor +PIXI.TilingSprite.prototype = Object.create(PIXI.Sprite.prototype); +PIXI.TilingSprite.prototype.constructor = PIXI.TilingSprite; + + +/** + * The width of the sprite, setting this will actually modify the scale to achieve the value set + * + * @property width + * @type Number + */ +Object.defineProperty(PIXI.TilingSprite.prototype, 'width', { + get: function() { + return this._width; + }, + set: function(value) { + + this._width = value; + } +}); + +/** + * The height of the TilingSprite, setting this will actually modify the scale to achieve the value set + * + * @property height + * @type Number + */ +Object.defineProperty(PIXI.TilingSprite.prototype, 'height', { + get: function() { + return this._height; + }, + set: function(value) { + this._height = value; + } +}); + +/** + * When the texture is updated, this event will be fired to update the scale and frame + * + * @method onTextureUpdate + * @param event + * @private + */ +PIXI.TilingSprite.prototype.onTextureUpdate = function() +{ + this.updateFrame = true; +}; + +PIXI.TilingSprite.prototype.setTexture = function(texture) +{ + if(this.texture === texture)return; + + this.texture = texture; + + this.refreshTexture = true; + /* + if(this.tilingTexture) + { + this.generateTilingTexture(true); + } +*/ + + /* + // stop current texture; + if(this.texture.baseTexture !== texture.baseTexture) + { + this.textureChange = true; + this.texture = texture; + } + else + { + this.texture = texture; + } + + this.updateFrame = true;*/ + this.cachedTint = 0xFFFFFF; +}; + +/** +* Renders the object using the WebGL renderer +* +* @method _renderWebGL +* @param renderSession {RenderSession} +* @private +*/ +PIXI.TilingSprite.prototype._renderWebGL = function(renderSession) +{ + + if(this.visible === false || this.alpha === 0)return; + + var i,j; + + if(this.mask) + { + renderSession.spriteBatch.stop(); + renderSession.maskManager.pushMask(this.mask, renderSession); + renderSession.spriteBatch.start(); + } + + if(this.filters) + { + renderSession.spriteBatch.flush(); + renderSession.filterManager.pushFilter(this._filterBlock); + } + + + if(!this.tilingTexture || this.refreshTexture) + { + this.generateTilingTexture(true); + if(this.tilingTexture && this.tilingTexture.needsUpdate) + { + //TODO - tweaking + PIXI.updateWebGLTexture(this.tilingTexture.baseTexture, renderSession.gl); + this.tilingTexture.needsUpdate = false; + // this.tilingTexture._uvs = null; + } + } + else renderSession.spriteBatch.renderTilingSprite(this); + + + // simple render children! + for(i=0,j=this.children.length; i maxX ? x1 : maxX; + maxX = x2 > maxX ? x2 : maxX; + maxX = x3 > maxX ? x3 : maxX; + maxX = x4 > maxX ? x4 : maxX; + + maxY = y1 > maxY ? y1 : maxY; + maxY = y2 > maxY ? y2 : maxY; + maxY = y3 > maxY ? y3 : maxY; + maxY = y4 > maxY ? y4 : maxY; + + var bounds = this._bounds; + + bounds.x = minX; + bounds.width = maxX - minX; + + bounds.y = minY; + bounds.height = maxY - minY; + + // store a reference so that if this function gets called again in the render cycle we do not have to recalculate + this._currentBounds = bounds; + + return bounds; +}; + +/** +* +* @method generateTilingTexture +* +* @param forcePowerOfTwo {Boolean} Whether we want to force the texture to be a power of two +*/ +PIXI.TilingSprite.prototype.generateTilingTexture = function(forcePowerOfTwo) +{ + var texture = this.texture; + + if(!texture.baseTexture.hasLoaded)return; + + var baseTexture = texture.baseTexture; + var frame = texture.frame; + + var targetWidth, targetHeight; + + // check that the frame is the same size as the base texture. + var isFrame = frame.width !== baseTexture.width || frame.height !== baseTexture.height; + + var newTextureRequired = false; + + if(!forcePowerOfTwo) + { + if(isFrame) + { + targetWidth = frame.width; + targetHeight = frame.height; + + newTextureRequired = true; + + } + } + else + { + targetWidth = PIXI.getNextPowerOfTwo(frame.width); + targetHeight = PIXI.getNextPowerOfTwo(frame.height); + if(frame.width !== targetWidth && frame.height !== targetHeight)newTextureRequired = true; + } + + if(newTextureRequired) + { + var canvasBuffer; + + if(this.tilingTexture && this.tilingTexture.isTiling) + { + canvasBuffer = this.tilingTexture.canvasBuffer; + canvasBuffer.resize(targetWidth, targetHeight); + this.tilingTexture.baseTexture.width = targetWidth; + this.tilingTexture.baseTexture.height = targetHeight; + this.tilingTexture.needsUpdate = true; + } + else + { + canvasBuffer = new PIXI.CanvasBuffer(targetWidth, targetHeight); + + this.tilingTexture = PIXI.Texture.fromCanvas(canvasBuffer.canvas); + this.tilingTexture.canvasBuffer = canvasBuffer; + this.tilingTexture.isTiling = true; + + } + + canvasBuffer.context.drawImage(texture.baseTexture.source, + frame.x, + frame.y, + frame.width, + frame.height, + 0, + 0, + targetWidth, + targetHeight); + + this.tileScaleOffset.x = frame.width / targetWidth; + this.tileScaleOffset.y = frame.height / targetHeight; + + } + else + { + //TODO - switching? + if(this.tilingTexture && this.tilingTexture.isTiling) + { + // destroy the tiling texture! + // TODO could store this somewhere? + this.tilingTexture.destroy(true); + } + + this.tileScaleOffset.x = 1; + this.tileScaleOffset.y = 1; + this.tilingTexture = texture; + } + this.refreshTexture = false; + this.tilingTexture.baseTexture._powerOf2 = true; +}; +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + * based on pixi impact spine implementation made by Eemeli Kelokorpi (@ekelokorpi) https://github.com/ekelokorpi + * + * Awesome JS run time provided by EsotericSoftware + * https://github.com/EsotericSoftware/spine-runtimes + * + */ + +/* + * Awesome JS run time provided by EsotericSoftware + * + * https://github.com/EsotericSoftware/spine-runtimes + * + */ + + + +var spine = {}; + +spine.BoneData = function (name, parent) { + this.name = name; + this.parent = parent; +}; +spine.BoneData.prototype = { + length: 0, + x: 0, y: 0, + rotation: 0, + scaleX: 1, scaleY: 1 +}; + +spine.SlotData = function (name, boneData) { + this.name = name; + this.boneData = boneData; +}; +spine.SlotData.prototype = { + r: 1, g: 1, b: 1, a: 1, + attachmentName: null +}; + +spine.Bone = function (boneData, parent) { + this.data = boneData; + this.parent = parent; + this.setToSetupPose(); +}; +spine.Bone.yDown = false; +spine.Bone.prototype = { + x: 0, y: 0, + rotation: 0, + scaleX: 1, scaleY: 1, + m00: 0, m01: 0, worldX: 0, // a b x + m10: 0, m11: 0, worldY: 0, // c d y + worldRotation: 0, + worldScaleX: 1, worldScaleY: 1, + updateWorldTransform: function (flipX, flipY) { + var parent = this.parent; + if (parent != null) { + this.worldX = this.x * parent.m00 + this.y * parent.m01 + parent.worldX; + this.worldY = this.x * parent.m10 + this.y * parent.m11 + parent.worldY; + this.worldScaleX = parent.worldScaleX * this.scaleX; + this.worldScaleY = parent.worldScaleY * this.scaleY; + this.worldRotation = parent.worldRotation + this.rotation; + } else { + this.worldX = this.x; + this.worldY = this.y; + this.worldScaleX = this.scaleX; + this.worldScaleY = this.scaleY; + this.worldRotation = this.rotation; + } + var radians = this.worldRotation * Math.PI / 180; + var cos = Math.cos(radians); + var sin = Math.sin(radians); + this.m00 = cos * this.worldScaleX; + this.m10 = sin * this.worldScaleX; + this.m01 = -sin * this.worldScaleY; + this.m11 = cos * this.worldScaleY; + if (flipX) { + this.m00 = -this.m00; + this.m01 = -this.m01; + } + if (flipY) { + this.m10 = -this.m10; + this.m11 = -this.m11; + } + if (spine.Bone.yDown) { + this.m10 = -this.m10; + this.m11 = -this.m11; + } + }, + setToSetupPose: function () { + var data = this.data; + this.x = data.x; + this.y = data.y; + this.rotation = data.rotation; + this.scaleX = data.scaleX; + this.scaleY = data.scaleY; + } +}; + +spine.Slot = function (slotData, skeleton, bone) { + this.data = slotData; + this.skeleton = skeleton; + this.bone = bone; + this.setToSetupPose(); +}; +spine.Slot.prototype = { + r: 1, g: 1, b: 1, a: 1, + _attachmentTime: 0, + attachment: null, + setAttachment: function (attachment) { + this.attachment = attachment; + this._attachmentTime = this.skeleton.time; + }, + setAttachmentTime: function (time) { + this._attachmentTime = this.skeleton.time - time; + }, + getAttachmentTime: function () { + return this.skeleton.time - this._attachmentTime; + }, + setToSetupPose: function () { + var data = this.data; + this.r = data.r; + this.g = data.g; + this.b = data.b; + this.a = data.a; + + var slotDatas = this.skeleton.data.slots; + for (var i = 0, n = slotDatas.length; i < n; i++) { + if (slotDatas[i] == data) { + this.setAttachment(!data.attachmentName ? null : this.skeleton.getAttachmentBySlotIndex(i, data.attachmentName)); + break; + } + } + } +}; + +spine.Skin = function (name) { + this.name = name; + this.attachments = {}; +}; +spine.Skin.prototype = { + addAttachment: function (slotIndex, name, attachment) { + this.attachments[slotIndex + ":" + name] = attachment; + }, + getAttachment: function (slotIndex, name) { + return this.attachments[slotIndex + ":" + name]; + }, + _attachAll: function (skeleton, oldSkin) { + for (var key in oldSkin.attachments) { + var colon = key.indexOf(":"); + var slotIndex = parseInt(key.substring(0, colon), 10); + var name = key.substring(colon + 1); + var slot = skeleton.slots[slotIndex]; + if (slot.attachment && slot.attachment.name == name) { + var attachment = this.getAttachment(slotIndex, name); + if (attachment) slot.setAttachment(attachment); + } + } + } +}; + +spine.Animation = function (name, timelines, duration) { + this.name = name; + this.timelines = timelines; + this.duration = duration; +}; +spine.Animation.prototype = { + apply: function (skeleton, time, loop) { + if (loop && this.duration) time %= this.duration; + var timelines = this.timelines; + for (var i = 0, n = timelines.length; i < n; i++) + timelines[i].apply(skeleton, time, 1); + }, + mix: function (skeleton, time, loop, alpha) { + if (loop && this.duration) time %= this.duration; + var timelines = this.timelines; + for (var i = 0, n = timelines.length; i < n; i++) + timelines[i].apply(skeleton, time, alpha); + } +}; + +spine.binarySearch = function (values, target, step) { + var low = 0; + var high = Math.floor(values.length / step) - 2; + if (!high) return step; + var current = high >>> 1; + while (true) { + if (values[(current + 1) * step] <= target) + low = current + 1; + else + high = current; + if (low == high) return (low + 1) * step; + current = (low + high) >>> 1; + } +}; +spine.linearSearch = function (values, target, step) { + for (var i = 0, last = values.length - step; i <= last; i += step) + if (values[i] > target) return i; + return -1; +}; + +spine.Curves = function (frameCount) { + this.curves = []; // dfx, dfy, ddfx, ddfy, dddfx, dddfy, ... + this.curves.length = (frameCount - 1) * 6; +}; +spine.Curves.prototype = { + setLinear: function (frameIndex) { + this.curves[frameIndex * 6] = 0/*LINEAR*/; + }, + setStepped: function (frameIndex) { + this.curves[frameIndex * 6] = -1/*STEPPED*/; + }, + /** Sets the control handle positions for an interpolation bezier curve used to transition from this keyframe to the next. + * cx1 and cx2 are from 0 to 1, representing the percent of time between the two keyframes. cy1 and cy2 are the percent of + * the difference between the keyframe's values. */ + setCurve: function (frameIndex, cx1, cy1, cx2, cy2) { + var subdiv_step = 1 / 10/*BEZIER_SEGMENTS*/; + var subdiv_step2 = subdiv_step * subdiv_step; + var subdiv_step3 = subdiv_step2 * subdiv_step; + var pre1 = 3 * subdiv_step; + var pre2 = 3 * subdiv_step2; + var pre4 = 6 * subdiv_step2; + var pre5 = 6 * subdiv_step3; + var tmp1x = -cx1 * 2 + cx2; + var tmp1y = -cy1 * 2 + cy2; + var tmp2x = (cx1 - cx2) * 3 + 1; + var tmp2y = (cy1 - cy2) * 3 + 1; + var i = frameIndex * 6; + var curves = this.curves; + curves[i] = cx1 * pre1 + tmp1x * pre2 + tmp2x * subdiv_step3; + curves[i + 1] = cy1 * pre1 + tmp1y * pre2 + tmp2y * subdiv_step3; + curves[i + 2] = tmp1x * pre4 + tmp2x * pre5; + curves[i + 3] = tmp1y * pre4 + tmp2y * pre5; + curves[i + 4] = tmp2x * pre5; + curves[i + 5] = tmp2y * pre5; + }, + getCurvePercent: function (frameIndex, percent) { + percent = percent < 0 ? 0 : (percent > 1 ? 1 : percent); + var curveIndex = frameIndex * 6; + var curves = this.curves; + var dfx = curves[curveIndex]; + if (!dfx/*LINEAR*/) return percent; + if (dfx == -1/*STEPPED*/) return 0; + var dfy = curves[curveIndex + 1]; + var ddfx = curves[curveIndex + 2]; + var ddfy = curves[curveIndex + 3]; + var dddfx = curves[curveIndex + 4]; + var dddfy = curves[curveIndex + 5]; + var x = dfx, y = dfy; + var i = 10/*BEZIER_SEGMENTS*/ - 2; + while (true) { + if (x >= percent) { + var lastX = x - dfx; + var lastY = y - dfy; + return lastY + (y - lastY) * (percent - lastX) / (x - lastX); + } + if (!i) break; + i--; + dfx += ddfx; + dfy += ddfy; + ddfx += dddfx; + ddfy += dddfy; + x += dfx; + y += dfy; + } + return y + (1 - y) * (percent - x) / (1 - x); // Last point is 1,1. + } +}; + +spine.RotateTimeline = function (frameCount) { + this.curves = new spine.Curves(frameCount); + this.frames = []; // time, angle, ... + this.frames.length = frameCount * 2; +}; +spine.RotateTimeline.prototype = { + boneIndex: 0, + getFrameCount: function () { + return this.frames.length / 2; + }, + setFrame: function (frameIndex, time, angle) { + frameIndex *= 2; + this.frames[frameIndex] = time; + this.frames[frameIndex + 1] = angle; + }, + apply: function (skeleton, time, alpha) { + var frames = this.frames, + amount; + + if (time < frames[0]) return; // Time is before first frame. + + var bone = skeleton.bones[this.boneIndex]; + + if (time >= frames[frames.length - 2]) { // Time is after last frame. + amount = bone.data.rotation + frames[frames.length - 1] - bone.rotation; + while (amount > 180) + amount -= 360; + while (amount < -180) + amount += 360; + bone.rotation += amount * alpha; + return; + } + + // Interpolate between the last frame and the current frame. + var frameIndex = spine.binarySearch(frames, time, 2); + var lastFrameValue = frames[frameIndex - 1]; + var frameTime = frames[frameIndex]; + var percent = 1 - (time - frameTime) / (frames[frameIndex - 2/*LAST_FRAME_TIME*/] - frameTime); + percent = this.curves.getCurvePercent(frameIndex / 2 - 1, percent); + + amount = frames[frameIndex + 1/*FRAME_VALUE*/] - lastFrameValue; + while (amount > 180) + amount -= 360; + while (amount < -180) + amount += 360; + amount = bone.data.rotation + (lastFrameValue + amount * percent) - bone.rotation; + while (amount > 180) + amount -= 360; + while (amount < -180) + amount += 360; + bone.rotation += amount * alpha; + } +}; + +spine.TranslateTimeline = function (frameCount) { + this.curves = new spine.Curves(frameCount); + this.frames = []; // time, x, y, ... + this.frames.length = frameCount * 3; +}; +spine.TranslateTimeline.prototype = { + boneIndex: 0, + getFrameCount: function () { + return this.frames.length / 3; + }, + setFrame: function (frameIndex, time, x, y) { + frameIndex *= 3; + this.frames[frameIndex] = time; + this.frames[frameIndex + 1] = x; + this.frames[frameIndex + 2] = y; + }, + apply: function (skeleton, time, alpha) { + var frames = this.frames; + if (time < frames[0]) return; // Time is before first frame. + + var bone = skeleton.bones[this.boneIndex]; + + if (time >= frames[frames.length - 3]) { // Time is after last frame. + bone.x += (bone.data.x + frames[frames.length - 2] - bone.x) * alpha; + bone.y += (bone.data.y + frames[frames.length - 1] - bone.y) * alpha; + return; + } + + // Interpolate between the last frame and the current frame. + var frameIndex = spine.binarySearch(frames, time, 3); + var lastFrameX = frames[frameIndex - 2]; + var lastFrameY = frames[frameIndex - 1]; + var frameTime = frames[frameIndex]; + var percent = 1 - (time - frameTime) / (frames[frameIndex + -3/*LAST_FRAME_TIME*/] - frameTime); + percent = this.curves.getCurvePercent(frameIndex / 3 - 1, percent); + + bone.x += (bone.data.x + lastFrameX + (frames[frameIndex + 1/*FRAME_X*/] - lastFrameX) * percent - bone.x) * alpha; + bone.y += (bone.data.y + lastFrameY + (frames[frameIndex + 2/*FRAME_Y*/] - lastFrameY) * percent - bone.y) * alpha; + } +}; + +spine.ScaleTimeline = function (frameCount) { + this.curves = new spine.Curves(frameCount); + this.frames = []; // time, x, y, ... + this.frames.length = frameCount * 3; +}; +spine.ScaleTimeline.prototype = { + boneIndex: 0, + getFrameCount: function () { + return this.frames.length / 3; + }, + setFrame: function (frameIndex, time, x, y) { + frameIndex *= 3; + this.frames[frameIndex] = time; + this.frames[frameIndex + 1] = x; + this.frames[frameIndex + 2] = y; + }, + apply: function (skeleton, time, alpha) { + var frames = this.frames; + if (time < frames[0]) return; // Time is before first frame. + + var bone = skeleton.bones[this.boneIndex]; + + if (time >= frames[frames.length - 3]) { // Time is after last frame. + bone.scaleX += (bone.data.scaleX - 1 + frames[frames.length - 2] - bone.scaleX) * alpha; + bone.scaleY += (bone.data.scaleY - 1 + frames[frames.length - 1] - bone.scaleY) * alpha; + return; + } + + // Interpolate between the last frame and the current frame. + var frameIndex = spine.binarySearch(frames, time, 3); + var lastFrameX = frames[frameIndex - 2]; + var lastFrameY = frames[frameIndex - 1]; + var frameTime = frames[frameIndex]; + var percent = 1 - (time - frameTime) / (frames[frameIndex + -3/*LAST_FRAME_TIME*/] - frameTime); + percent = this.curves.getCurvePercent(frameIndex / 3 - 1, percent); + + bone.scaleX += (bone.data.scaleX - 1 + lastFrameX + (frames[frameIndex + 1/*FRAME_X*/] - lastFrameX) * percent - bone.scaleX) * alpha; + bone.scaleY += (bone.data.scaleY - 1 + lastFrameY + (frames[frameIndex + 2/*FRAME_Y*/] - lastFrameY) * percent - bone.scaleY) * alpha; + } +}; + +spine.ColorTimeline = function (frameCount) { + this.curves = new spine.Curves(frameCount); + this.frames = []; // time, r, g, b, a, ... + this.frames.length = frameCount * 5; +}; +spine.ColorTimeline.prototype = { + slotIndex: 0, + getFrameCount: function () { + return this.frames.length / 2; + }, + setFrame: function (frameIndex, time, x, y) { + frameIndex *= 5; + this.frames[frameIndex] = time; + this.frames[frameIndex + 1] = r; + this.frames[frameIndex + 2] = g; + this.frames[frameIndex + 3] = b; + this.frames[frameIndex + 4] = a; + }, + apply: function (skeleton, time, alpha) { + var frames = this.frames; + if (time < frames[0]) return; // Time is before first frame. + + var slot = skeleton.slots[this.slotIndex]; + + if (time >= frames[frames.length - 5]) { // Time is after last frame. + var i = frames.length - 1; + slot.r = frames[i - 3]; + slot.g = frames[i - 2]; + slot.b = frames[i - 1]; + slot.a = frames[i]; + return; + } + + // Interpolate between the last frame and the current frame. + var frameIndex = spine.binarySearch(frames, time, 5); + var lastFrameR = frames[frameIndex - 4]; + var lastFrameG = frames[frameIndex - 3]; + var lastFrameB = frames[frameIndex - 2]; + var lastFrameA = frames[frameIndex - 1]; + var frameTime = frames[frameIndex]; + var percent = 1 - (time - frameTime) / (frames[frameIndex - 5/*LAST_FRAME_TIME*/] - frameTime); + percent = this.curves.getCurvePercent(frameIndex / 5 - 1, percent); + + var r = lastFrameR + (frames[frameIndex + 1/*FRAME_R*/] - lastFrameR) * percent; + var g = lastFrameG + (frames[frameIndex + 2/*FRAME_G*/] - lastFrameG) * percent; + var b = lastFrameB + (frames[frameIndex + 3/*FRAME_B*/] - lastFrameB) * percent; + var a = lastFrameA + (frames[frameIndex + 4/*FRAME_A*/] - lastFrameA) * percent; + if (alpha < 1) { + slot.r += (r - slot.r) * alpha; + slot.g += (g - slot.g) * alpha; + slot.b += (b - slot.b) * alpha; + slot.a += (a - slot.a) * alpha; + } else { + slot.r = r; + slot.g = g; + slot.b = b; + slot.a = a; + } + } +}; + +spine.AttachmentTimeline = function (frameCount) { + this.curves = new spine.Curves(frameCount); + this.frames = []; // time, ... + this.frames.length = frameCount; + this.attachmentNames = []; // time, ... + this.attachmentNames.length = frameCount; +}; +spine.AttachmentTimeline.prototype = { + slotIndex: 0, + getFrameCount: function () { + return this.frames.length; + }, + setFrame: function (frameIndex, time, attachmentName) { + this.frames[frameIndex] = time; + this.attachmentNames[frameIndex] = attachmentName; + }, + apply: function (skeleton, time, alpha) { + var frames = this.frames; + if (time < frames[0]) return; // Time is before first frame. + + var frameIndex; + if (time >= frames[frames.length - 1]) // Time is after last frame. + frameIndex = frames.length - 1; + else + frameIndex = spine.binarySearch(frames, time, 1) - 1; + + var attachmentName = this.attachmentNames[frameIndex]; + skeleton.slots[this.slotIndex].setAttachment(!attachmentName ? null : skeleton.getAttachmentBySlotIndex(this.slotIndex, attachmentName)); + } +}; + +spine.SkeletonData = function () { + this.bones = []; + this.slots = []; + this.skins = []; + this.animations = []; +}; +spine.SkeletonData.prototype = { + defaultSkin: null, + /** @return May be null. */ + findBone: function (boneName) { + var bones = this.bones; + for (var i = 0, n = bones.length; i < n; i++) + if (bones[i].name == boneName) return bones[i]; + return null; + }, + /** @return -1 if the bone was not found. */ + findBoneIndex: function (boneName) { + var bones = this.bones; + for (var i = 0, n = bones.length; i < n; i++) + if (bones[i].name == boneName) return i; + return -1; + }, + /** @return May be null. */ + findSlot: function (slotName) { + var slots = this.slots; + for (var i = 0, n = slots.length; i < n; i++) { + if (slots[i].name == slotName) return slot[i]; + } + return null; + }, + /** @return -1 if the bone was not found. */ + findSlotIndex: function (slotName) { + var slots = this.slots; + for (var i = 0, n = slots.length; i < n; i++) + if (slots[i].name == slotName) return i; + return -1; + }, + /** @return May be null. */ + findSkin: function (skinName) { + var skins = this.skins; + for (var i = 0, n = skins.length; i < n; i++) + if (skins[i].name == skinName) return skins[i]; + return null; + }, + /** @return May be null. */ + findAnimation: function (animationName) { + var animations = this.animations; + for (var i = 0, n = animations.length; i < n; i++) + if (animations[i].name == animationName) return animations[i]; + return null; + } +}; + +spine.Skeleton = function (skeletonData) { + this.data = skeletonData; + + this.bones = []; + for (var i = 0, n = skeletonData.bones.length; i < n; i++) { + var boneData = skeletonData.bones[i]; + var parent = !boneData.parent ? null : this.bones[skeletonData.bones.indexOf(boneData.parent)]; + this.bones.push(new spine.Bone(boneData, parent)); + } + + this.slots = []; + this.drawOrder = []; + for (i = 0, n = skeletonData.slots.length; i < n; i++) { + var slotData = skeletonData.slots[i]; + var bone = this.bones[skeletonData.bones.indexOf(slotData.boneData)]; + var slot = new spine.Slot(slotData, this, bone); + this.slots.push(slot); + this.drawOrder.push(slot); + } +}; +spine.Skeleton.prototype = { + x: 0, y: 0, + skin: null, + r: 1, g: 1, b: 1, a: 1, + time: 0, + flipX: false, flipY: false, + /** Updates the world transform for each bone. */ + updateWorldTransform: function () { + var flipX = this.flipX; + var flipY = this.flipY; + var bones = this.bones; + for (var i = 0, n = bones.length; i < n; i++) + bones[i].updateWorldTransform(flipX, flipY); + }, + /** Sets the bones and slots to their setup pose values. */ + setToSetupPose: function () { + this.setBonesToSetupPose(); + this.setSlotsToSetupPose(); + }, + setBonesToSetupPose: function () { + var bones = this.bones; + for (var i = 0, n = bones.length; i < n; i++) + bones[i].setToSetupPose(); + }, + setSlotsToSetupPose: function () { + var slots = this.slots; + for (var i = 0, n = slots.length; i < n; i++) + slots[i].setToSetupPose(i); + }, + /** @return May return null. */ + getRootBone: function () { + return this.bones.length ? this.bones[0] : null; + }, + /** @return May be null. */ + findBone: function (boneName) { + var bones = this.bones; + for (var i = 0, n = bones.length; i < n; i++) + if (bones[i].data.name == boneName) return bones[i]; + return null; + }, + /** @return -1 if the bone was not found. */ + findBoneIndex: function (boneName) { + var bones = this.bones; + for (var i = 0, n = bones.length; i < n; i++) + if (bones[i].data.name == boneName) return i; + return -1; + }, + /** @return May be null. */ + findSlot: function (slotName) { + var slots = this.slots; + for (var i = 0, n = slots.length; i < n; i++) + if (slots[i].data.name == slotName) return slots[i]; + return null; + }, + /** @return -1 if the bone was not found. */ + findSlotIndex: function (slotName) { + var slots = this.slots; + for (var i = 0, n = slots.length; i < n; i++) + if (slots[i].data.name == slotName) return i; + return -1; + }, + setSkinByName: function (skinName) { + var skin = this.data.findSkin(skinName); + if (!skin) throw "Skin not found: " + skinName; + this.setSkin(skin); + }, + /** Sets the skin used to look up attachments not found in the {@link SkeletonData#getDefaultSkin() default skin}. Attachments + * from the new skin are attached if the corresponding attachment from the old skin was attached. + * @param newSkin May be null. */ + setSkin: function (newSkin) { + if (this.skin && newSkin) newSkin._attachAll(this, this.skin); + this.skin = newSkin; + }, + /** @return May be null. */ + getAttachmentBySlotName: function (slotName, attachmentName) { + return this.getAttachmentBySlotIndex(this.data.findSlotIndex(slotName), attachmentName); + }, + /** @return May be null. */ + getAttachmentBySlotIndex: function (slotIndex, attachmentName) { + if (this.skin) { + var attachment = this.skin.getAttachment(slotIndex, attachmentName); + if (attachment) return attachment; + } + if (this.data.defaultSkin) return this.data.defaultSkin.getAttachment(slotIndex, attachmentName); + return null; + }, + /** @param attachmentName May be null. */ + setAttachment: function (slotName, attachmentName) { + var slots = this.slots; + for (var i = 0, n = slots.size; i < n; i++) { + var slot = slots[i]; + if (slot.data.name == slotName) { + var attachment = null; + if (attachmentName) { + attachment = this.getAttachment(i, attachmentName); + if (attachment == null) throw "Attachment not found: " + attachmentName + ", for slot: " + slotName; + } + slot.setAttachment(attachment); + return; + } + } + throw "Slot not found: " + slotName; + }, + update: function (delta) { + time += delta; + } +}; + +spine.AttachmentType = { + region: 0 +}; + +spine.RegionAttachment = function () { + this.offset = []; + this.offset.length = 8; + this.uvs = []; + this.uvs.length = 8; +}; +spine.RegionAttachment.prototype = { + x: 0, y: 0, + rotation: 0, + scaleX: 1, scaleY: 1, + width: 0, height: 0, + rendererObject: null, + regionOffsetX: 0, regionOffsetY: 0, + regionWidth: 0, regionHeight: 0, + regionOriginalWidth: 0, regionOriginalHeight: 0, + setUVs: function (u, v, u2, v2, rotate) { + var uvs = this.uvs; + if (rotate) { + uvs[2/*X2*/] = u; + uvs[3/*Y2*/] = v2; + uvs[4/*X3*/] = u; + uvs[5/*Y3*/] = v; + uvs[6/*X4*/] = u2; + uvs[7/*Y4*/] = v; + uvs[0/*X1*/] = u2; + uvs[1/*Y1*/] = v2; + } else { + uvs[0/*X1*/] = u; + uvs[1/*Y1*/] = v2; + uvs[2/*X2*/] = u; + uvs[3/*Y2*/] = v; + uvs[4/*X3*/] = u2; + uvs[5/*Y3*/] = v; + uvs[6/*X4*/] = u2; + uvs[7/*Y4*/] = v2; + } + }, + updateOffset: function () { + var regionScaleX = this.width / this.regionOriginalWidth * this.scaleX; + var regionScaleY = this.height / this.regionOriginalHeight * this.scaleY; + var localX = -this.width / 2 * this.scaleX + this.regionOffsetX * regionScaleX; + var localY = -this.height / 2 * this.scaleY + this.regionOffsetY * regionScaleY; + var localX2 = localX + this.regionWidth * regionScaleX; + var localY2 = localY + this.regionHeight * regionScaleY; + var radians = this.rotation * Math.PI / 180; + var cos = Math.cos(radians); + var sin = Math.sin(radians); + var localXCos = localX * cos + this.x; + var localXSin = localX * sin; + var localYCos = localY * cos + this.y; + var localYSin = localY * sin; + var localX2Cos = localX2 * cos + this.x; + var localX2Sin = localX2 * sin; + var localY2Cos = localY2 * cos + this.y; + var localY2Sin = localY2 * sin; + var offset = this.offset; + offset[0/*X1*/] = localXCos - localYSin; + offset[1/*Y1*/] = localYCos + localXSin; + offset[2/*X2*/] = localXCos - localY2Sin; + offset[3/*Y2*/] = localY2Cos + localXSin; + offset[4/*X3*/] = localX2Cos - localY2Sin; + offset[5/*Y3*/] = localY2Cos + localX2Sin; + offset[6/*X4*/] = localX2Cos - localYSin; + offset[7/*Y4*/] = localYCos + localX2Sin; + }, + computeVertices: function (x, y, bone, vertices) { + x += bone.worldX; + y += bone.worldY; + var m00 = bone.m00; + var m01 = bone.m01; + var m10 = bone.m10; + var m11 = bone.m11; + var offset = this.offset; + vertices[0/*X1*/] = offset[0/*X1*/] * m00 + offset[1/*Y1*/] * m01 + x; + vertices[1/*Y1*/] = offset[0/*X1*/] * m10 + offset[1/*Y1*/] * m11 + y; + vertices[2/*X2*/] = offset[2/*X2*/] * m00 + offset[3/*Y2*/] * m01 + x; + vertices[3/*Y2*/] = offset[2/*X2*/] * m10 + offset[3/*Y2*/] * m11 + y; + vertices[4/*X3*/] = offset[4/*X3*/] * m00 + offset[5/*X3*/] * m01 + x; + vertices[5/*X3*/] = offset[4/*X3*/] * m10 + offset[5/*X3*/] * m11 + y; + vertices[6/*X4*/] = offset[6/*X4*/] * m00 + offset[7/*Y4*/] * m01 + x; + vertices[7/*Y4*/] = offset[6/*X4*/] * m10 + offset[7/*Y4*/] * m11 + y; + } +} + +spine.AnimationStateData = function (skeletonData) { + this.skeletonData = skeletonData; + this.animationToMixTime = {}; +}; +spine.AnimationStateData.prototype = { + defaultMix: 0, + setMixByName: function (fromName, toName, duration) { + var from = this.skeletonData.findAnimation(fromName); + if (!from) throw "Animation not found: " + fromName; + var to = this.skeletonData.findAnimation(toName); + if (!to) throw "Animation not found: " + toName; + this.setMix(from, to, duration); + }, + setMix: function (from, to, duration) { + this.animationToMixTime[from.name + ":" + to.name] = duration; + }, + getMix: function (from, to) { + var time = this.animationToMixTime[from.name + ":" + to.name]; + return time ? time : this.defaultMix; + } +}; + +spine.AnimationState = function (stateData) { + this.data = stateData; + this.queue = []; +}; +spine.AnimationState.prototype = { + current: null, + previous: null, + currentTime: 0, + previousTime: 0, + currentLoop: false, + previousLoop: false, + mixTime: 0, + mixDuration: 0, + update: function (delta) { + this.currentTime += delta; + this.previousTime += delta; + this.mixTime += delta; + + if (this.queue.length > 0) { + var entry = this.queue[0]; + if (this.currentTime >= entry.delay) { + this._setAnimation(entry.animation, entry.loop); + this.queue.shift(); + } + } + }, + apply: function (skeleton) { + if (!this.current) return; + if (this.previous) { + this.previous.apply(skeleton, this.previousTime, this.previousLoop); + var alpha = this.mixTime / this.mixDuration; + if (alpha >= 1) { + alpha = 1; + this.previous = null; + } + this.current.mix(skeleton, this.currentTime, this.currentLoop, alpha); + } else + this.current.apply(skeleton, this.currentTime, this.currentLoop); + }, + clearAnimation: function () { + this.previous = null; + this.current = null; + this.queue.length = 0; + }, + _setAnimation: function (animation, loop) { + this.previous = null; + if (animation && this.current) { + this.mixDuration = this.data.getMix(this.current, animation); + if (this.mixDuration > 0) { + this.mixTime = 0; + this.previous = this.current; + this.previousTime = this.currentTime; + this.previousLoop = this.currentLoop; + } + } + this.current = animation; + this.currentLoop = loop; + this.currentTime = 0; + }, + /** @see #setAnimation(Animation, Boolean) */ + setAnimationByName: function (animationName, loop) { + var animation = this.data.skeletonData.findAnimation(animationName); + if (!animation) throw "Animation not found: " + animationName; + this.setAnimation(animation, loop); + }, + /** Set the current animation. Any queued animations are cleared and the current animation time is set to 0. + * @param animation May be null. */ + setAnimation: function (animation, loop) { + this.queue.length = 0; + this._setAnimation(animation, loop); + }, + /** @see #addAnimation(Animation, Boolean, Number) */ + addAnimationByName: function (animationName, loop, delay) { + var animation = this.data.skeletonData.findAnimation(animationName); + if (!animation) throw "Animation not found: " + animationName; + this.addAnimation(animation, loop, delay); + }, + /** Adds an animation to be played delay seconds after the current or last queued animation. + * @param delay May be <= 0 to use duration of previous animation minus any mix duration plus the negative delay. */ + addAnimation: function (animation, loop, delay) { + var entry = {}; + entry.animation = animation; + entry.loop = loop; + + if (!delay || delay <= 0) { + var previousAnimation = this.queue.length ? this.queue[this.queue.length - 1].animation : this.current; + if (previousAnimation != null) + delay = previousAnimation.duration - this.data.getMix(previousAnimation, animation) + (delay || 0); + else + delay = 0; + } + entry.delay = delay; + + this.queue.push(entry); + }, + /** Returns true if no animation is set or if the current time is greater than the animation duration, regardless of looping. */ + isComplete: function () { + return !this.current || this.currentTime >= this.current.duration; + } +}; + +spine.SkeletonJson = function (attachmentLoader) { + this.attachmentLoader = attachmentLoader; +}; +spine.SkeletonJson.prototype = { + scale: 1, + readSkeletonData: function (root) { + /*jshint -W069*/ + var skeletonData = new spine.SkeletonData(), + boneData; + + // Bones. + var bones = root["bones"]; + for (var i = 0, n = bones.length; i < n; i++) { + var boneMap = bones[i]; + var parent = null; + if (boneMap["parent"]) { + parent = skeletonData.findBone(boneMap["parent"]); + if (!parent) throw "Parent bone not found: " + boneMap["parent"]; + } + boneData = new spine.BoneData(boneMap["name"], parent); + boneData.length = (boneMap["length"] || 0) * this.scale; + boneData.x = (boneMap["x"] || 0) * this.scale; + boneData.y = (boneMap["y"] || 0) * this.scale; + boneData.rotation = (boneMap["rotation"] || 0); + boneData.scaleX = boneMap["scaleX"] || 1; + boneData.scaleY = boneMap["scaleY"] || 1; + skeletonData.bones.push(boneData); + } + + // Slots. + var slots = root["slots"]; + for (i = 0, n = slots.length; i < n; i++) { + var slotMap = slots[i]; + boneData = skeletonData.findBone(slotMap["bone"]); + if (!boneData) throw "Slot bone not found: " + slotMap["bone"]; + var slotData = new spine.SlotData(slotMap["name"], boneData); + + var color = slotMap["color"]; + if (color) { + slotData.r = spine.SkeletonJson.toColor(color, 0); + slotData.g = spine.SkeletonJson.toColor(color, 1); + slotData.b = spine.SkeletonJson.toColor(color, 2); + slotData.a = spine.SkeletonJson.toColor(color, 3); + } + + slotData.attachmentName = slotMap["attachment"]; + + skeletonData.slots.push(slotData); + } + + // Skins. + var skins = root["skins"]; + for (var skinName in skins) { + if (!skins.hasOwnProperty(skinName)) continue; + var skinMap = skins[skinName]; + var skin = new spine.Skin(skinName); + for (var slotName in skinMap) { + if (!skinMap.hasOwnProperty(slotName)) continue; + var slotIndex = skeletonData.findSlotIndex(slotName); + var slotEntry = skinMap[slotName]; + for (var attachmentName in slotEntry) { + if (!slotEntry.hasOwnProperty(attachmentName)) continue; + var attachment = this.readAttachment(skin, attachmentName, slotEntry[attachmentName]); + if (attachment != null) skin.addAttachment(slotIndex, attachmentName, attachment); + } + } + skeletonData.skins.push(skin); + if (skin.name == "default") skeletonData.defaultSkin = skin; + } + + // Animations. + var animations = root["animations"]; + for (var animationName in animations) { + if (!animations.hasOwnProperty(animationName)) continue; + this.readAnimation(animationName, animations[animationName], skeletonData); + } + + return skeletonData; + }, + readAttachment: function (skin, name, map) { + /*jshint -W069*/ + name = map["name"] || name; + + var type = spine.AttachmentType[map["type"] || "region"]; + + if (type == spine.AttachmentType.region) { + var attachment = new spine.RegionAttachment(); + attachment.x = (map["x"] || 0) * this.scale; + attachment.y = (map["y"] || 0) * this.scale; + attachment.scaleX = map["scaleX"] || 1; + attachment.scaleY = map["scaleY"] || 1; + attachment.rotation = map["rotation"] || 0; + attachment.width = (map["width"] || 32) * this.scale; + attachment.height = (map["height"] || 32) * this.scale; + attachment.updateOffset(); + + attachment.rendererObject = {}; + attachment.rendererObject.name = name; + attachment.rendererObject.scale = {}; + attachment.rendererObject.scale.x = attachment.scaleX; + attachment.rendererObject.scale.y = attachment.scaleY; + attachment.rendererObject.rotation = -attachment.rotation * Math.PI / 180; + return attachment; + } + + throw "Unknown attachment type: " + type; + }, + + readAnimation: function (name, map, skeletonData) { + /*jshint -W069*/ + var timelines = []; + var duration = 0; + var frameIndex, timeline, timelineName, valueMap, values, + i, n; + + var bones = map["bones"]; + for (var boneName in bones) { + if (!bones.hasOwnProperty(boneName)) continue; + var boneIndex = skeletonData.findBoneIndex(boneName); + if (boneIndex == -1) throw "Bone not found: " + boneName; + var boneMap = bones[boneName]; + + for (timelineName in boneMap) { + if (!boneMap.hasOwnProperty(timelineName)) continue; + values = boneMap[timelineName]; + if (timelineName == "rotate") { + timeline = new spine.RotateTimeline(values.length); + timeline.boneIndex = boneIndex; + + frameIndex = 0; + for (i = 0, n = values.length; i < n; i++) { + valueMap = values[i]; + timeline.setFrame(frameIndex, valueMap["time"], valueMap["angle"]); + spine.SkeletonJson.readCurve(timeline, frameIndex, valueMap); + frameIndex++; + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[timeline.getFrameCount() * 2 - 2]); + + } else if (timelineName == "translate" || timelineName == "scale") { + var timelineScale = 1; + if (timelineName == "scale") + timeline = new spine.ScaleTimeline(values.length); + else { + timeline = new spine.TranslateTimeline(values.length); + timelineScale = this.scale; + } + timeline.boneIndex = boneIndex; + + frameIndex = 0; + for (i = 0, n = values.length; i < n; i++) { + valueMap = values[i]; + var x = (valueMap["x"] || 0) * timelineScale; + var y = (valueMap["y"] || 0) * timelineScale; + timeline.setFrame(frameIndex, valueMap["time"], x, y); + spine.SkeletonJson.readCurve(timeline, frameIndex, valueMap); + frameIndex++; + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[timeline.getFrameCount() * 3 - 3]); + + } else + throw "Invalid timeline type for a bone: " + timelineName + " (" + boneName + ")"; + } + } + var slots = map["slots"]; + for (var slotName in slots) { + if (!slots.hasOwnProperty(slotName)) continue; + var slotMap = slots[slotName]; + var slotIndex = skeletonData.findSlotIndex(slotName); + + for (timelineName in slotMap) { + if (!slotMap.hasOwnProperty(timelineName)) continue; + values = slotMap[timelineName]; + if (timelineName == "color") { + timeline = new spine.ColorTimeline(values.length); + timeline.slotIndex = slotIndex; + + frameIndex = 0; + for (i = 0, n = values.length; i < n; i++) { + valueMap = values[i]; + var color = valueMap["color"]; + var r = spine.SkeletonJson.toColor(color, 0); + var g = spine.SkeletonJson.toColor(color, 1); + var b = spine.SkeletonJson.toColor(color, 2); + var a = spine.SkeletonJson.toColor(color, 3); + timeline.setFrame(frameIndex, valueMap["time"], r, g, b, a); + spine.SkeletonJson.readCurve(timeline, frameIndex, valueMap); + frameIndex++; + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[timeline.getFrameCount() * 5 - 5]); + + } else if (timelineName == "attachment") { + timeline = new spine.AttachmentTimeline(values.length); + timeline.slotIndex = slotIndex; + + frameIndex = 0; + for (i = 0, n = values.length; i < n; i++) { + valueMap = values[i]; + timeline.setFrame(frameIndex++, valueMap["time"], valueMap["name"]); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[timeline.getFrameCount() - 1]); + + } else + throw "Invalid timeline type for a slot: " + timelineName + " (" + slotName + ")"; + } + } + skeletonData.animations.push(new spine.Animation(name, timelines, duration)); + } +}; +spine.SkeletonJson.readCurve = function (timeline, frameIndex, valueMap) { + /*jshint -W069*/ + var curve = valueMap["curve"]; + if (!curve) return; + if (curve == "stepped") + timeline.curves.setStepped(frameIndex); + else if (curve instanceof Array) + timeline.curves.setCurve(frameIndex, curve[0], curve[1], curve[2], curve[3]); +}; +spine.SkeletonJson.toColor = function (hexString, colorIndex) { + if (hexString.length != 8) throw "Color hexidecimal length must be 8, recieved: " + hexString; + return parseInt(hexString.substring(colorIndex * 2, 2), 16) / 255; +}; + +spine.Atlas = function (atlasText, textureLoader) { + this.textureLoader = textureLoader; + this.pages = []; + this.regions = []; + + var reader = new spine.AtlasReader(atlasText); + var tuple = []; + tuple.length = 4; + var page = null; + while (true) { + var line = reader.readLine(); + if (line == null) break; + line = reader.trim(line); + if (!line.length) + page = null; + else if (!page) { + page = new spine.AtlasPage(); + page.name = line; + + page.format = spine.Atlas.Format[reader.readValue()]; + + reader.readTuple(tuple); + page.minFilter = spine.Atlas.TextureFilter[tuple[0]]; + page.magFilter = spine.Atlas.TextureFilter[tuple[1]]; + + var direction = reader.readValue(); + page.uWrap = spine.Atlas.TextureWrap.clampToEdge; + page.vWrap = spine.Atlas.TextureWrap.clampToEdge; + if (direction == "x") + page.uWrap = spine.Atlas.TextureWrap.repeat; + else if (direction == "y") + page.vWrap = spine.Atlas.TextureWrap.repeat; + else if (direction == "xy") + page.uWrap = page.vWrap = spine.Atlas.TextureWrap.repeat; + + textureLoader.load(page, line); + + this.pages.push(page); + + } else { + var region = new spine.AtlasRegion(); + region.name = line; + region.page = page; + + region.rotate = reader.readValue() == "true"; + + reader.readTuple(tuple); + var x = parseInt(tuple[0], 10); + var y = parseInt(tuple[1], 10); + + reader.readTuple(tuple); + var width = parseInt(tuple[0], 10); + var height = parseInt(tuple[1], 10); + + region.u = x / page.width; + region.v = y / page.height; + if (region.rotate) { + region.u2 = (x + height) / page.width; + region.v2 = (y + width) / page.height; + } else { + region.u2 = (x + width) / page.width; + region.v2 = (y + height) / page.height; + } + region.x = x; + region.y = y; + region.width = Math.abs(width); + region.height = Math.abs(height); + + if (reader.readTuple(tuple) == 4) { // split is optional + region.splits = [parseInt(tuple[0], 10), parseInt(tuple[1], 10), parseInt(tuple[2], 10), parseInt(tuple[3], 10)]; + + if (reader.readTuple(tuple) == 4) { // pad is optional, but only present with splits + region.pads = [parseInt(tuple[0], 10), parseInt(tuple[1], 10), parseInt(tuple[2], 10), parseInt(tuple[3], 10)]; + + reader.readTuple(tuple); + } + } + + region.originalWidth = parseInt(tuple[0], 10); + region.originalHeight = parseInt(tuple[1], 10); + + reader.readTuple(tuple); + region.offsetX = parseInt(tuple[0], 10); + region.offsetY = parseInt(tuple[1], 10); + + region.index = parseInt(reader.readValue(), 10); + + this.regions.push(region); + } + } +}; +spine.Atlas.prototype = { + findRegion: function (name) { + var regions = this.regions; + for (var i = 0, n = regions.length; i < n; i++) + if (regions[i].name == name) return regions[i]; + return null; + }, + dispose: function () { + var pages = this.pages; + for (var i = 0, n = pages.length; i < n; i++) + this.textureLoader.unload(pages[i].rendererObject); + }, + updateUVs: function (page) { + var regions = this.regions; + for (var i = 0, n = regions.length; i < n; i++) { + var region = regions[i]; + if (region.page != page) continue; + region.u = region.x / page.width; + region.v = region.y / page.height; + if (region.rotate) { + region.u2 = (region.x + region.height) / page.width; + region.v2 = (region.y + region.width) / page.height; + } else { + region.u2 = (region.x + region.width) / page.width; + region.v2 = (region.y + region.height) / page.height; + } + } + } +}; + +spine.Atlas.Format = { + alpha: 0, + intensity: 1, + luminanceAlpha: 2, + rgb565: 3, + rgba4444: 4, + rgb888: 5, + rgba8888: 6 +}; + +spine.Atlas.TextureFilter = { + nearest: 0, + linear: 1, + mipMap: 2, + mipMapNearestNearest: 3, + mipMapLinearNearest: 4, + mipMapNearestLinear: 5, + mipMapLinearLinear: 6 +}; + +spine.Atlas.TextureWrap = { + mirroredRepeat: 0, + clampToEdge: 1, + repeat: 2 +}; + +spine.AtlasPage = function () {}; +spine.AtlasPage.prototype = { + name: null, + format: null, + minFilter: null, + magFilter: null, + uWrap: null, + vWrap: null, + rendererObject: null, + width: 0, + height: 0 +}; + +spine.AtlasRegion = function () {}; +spine.AtlasRegion.prototype = { + page: null, + name: null, + x: 0, y: 0, + width: 0, height: 0, + u: 0, v: 0, u2: 0, v2: 0, + offsetX: 0, offsetY: 0, + originalWidth: 0, originalHeight: 0, + index: 0, + rotate: false, + splits: null, + pads: null, +}; + +spine.AtlasReader = function (text) { + this.lines = text.split(/\r\n|\r|\n/); +}; +spine.AtlasReader.prototype = { + index: 0, + trim: function (value) { + return value.replace(/^\s+|\s+$/g, ""); + }, + readLine: function () { + if (this.index >= this.lines.length) return null; + return this.lines[this.index++]; + }, + readValue: function () { + var line = this.readLine(); + var colon = line.indexOf(":"); + if (colon == -1) throw "Invalid line: " + line; + return this.trim(line.substring(colon + 1)); + }, + /** Returns the number of tuple values read (2 or 4). */ + readTuple: function (tuple) { + var line = this.readLine(); + var colon = line.indexOf(":"); + if (colon == -1) throw "Invalid line: " + line; + var i = 0, lastMatch= colon + 1; + for (; i < 3; i++) { + var comma = line.indexOf(",", lastMatch); + if (comma == -1) { + if (!i) throw "Invalid line: " + line; + break; + } + tuple[i] = this.trim(line.substr(lastMatch, comma - lastMatch)); + lastMatch = comma + 1; + } + tuple[i] = this.trim(line.substring(lastMatch)); + return i + 1; + } +} + +spine.AtlasAttachmentLoader = function (atlas) { + this.atlas = atlas; +} +spine.AtlasAttachmentLoader.prototype = { + newAttachment: function (skin, type, name) { + switch (type) { + case spine.AttachmentType.region: + var region = this.atlas.findRegion(name); + if (!region) throw "Region not found in atlas: " + name + " (" + type + ")"; + var attachment = new spine.RegionAttachment(name); + attachment.rendererObject = region; + attachment.setUVs(region.u, region.v, region.u2, region.v2, region.rotate); + attachment.regionOffsetX = region.offsetX; + attachment.regionOffsetY = region.offsetY; + attachment.regionWidth = region.width; + attachment.regionHeight = region.height; + attachment.regionOriginalWidth = region.originalWidth; + attachment.regionOriginalHeight = region.originalHeight; + return attachment; + } + throw "Unknown attachment type: " + type; + } +} + +spine.Bone.yDown = true; +PIXI.AnimCache = {}; + +/** + * A class that enables the you to import and run your spine animations in pixi. + * Spine animation data needs to be loaded using the PIXI.AssetLoader or PIXI.SpineLoader before it can be used by this class + * See example 12 (http://www.goodboydigital.com/pixijs/examples/12/) to see a working example and check out the source + * + * @class Spine + * @extends DisplayObjectContainer + * @constructor + * @param url {String} The url of the spine anim file to be used + */ +PIXI.Spine = function (url) { + PIXI.DisplayObjectContainer.call(this); + + this.spineData = PIXI.AnimCache[url]; + + if (!this.spineData) { + throw new Error("Spine data must be preloaded using PIXI.SpineLoader or PIXI.AssetLoader: " + url); + } + + this.skeleton = new spine.Skeleton(this.spineData); + this.skeleton.updateWorldTransform(); + + this.stateData = new spine.AnimationStateData(this.spineData); + this.state = new spine.AnimationState(this.stateData); + + this.slotContainers = []; + + for (var i = 0, n = this.skeleton.drawOrder.length; i < n; i++) { + var slot = this.skeleton.drawOrder[i]; + var attachment = slot.attachment; + var slotContainer = new PIXI.DisplayObjectContainer(); + this.slotContainers.push(slotContainer); + this.addChild(slotContainer); + if (!(attachment instanceof spine.RegionAttachment)) { + continue; + } + var spriteName = attachment.rendererObject.name; + var sprite = this.createSprite(slot, attachment.rendererObject); + slot.currentSprite = sprite; + slot.currentSpriteName = spriteName; + slotContainer.addChild(sprite); + } +}; + +PIXI.Spine.prototype = Object.create(PIXI.DisplayObjectContainer.prototype); +PIXI.Spine.prototype.constructor = PIXI.Spine; + +/* + * Updates the object transform for rendering + * + * @method updateTransform + * @private + */ +PIXI.Spine.prototype.updateTransform = function () { + this.lastTime = this.lastTime || Date.now(); + var timeDelta = (Date.now() - this.lastTime) * 0.001; + this.lastTime = Date.now(); + this.state.update(timeDelta); + this.state.apply(this.skeleton); + this.skeleton.updateWorldTransform(); + + var drawOrder = this.skeleton.drawOrder; + for (var i = 0, n = drawOrder.length; i < n; i++) { + var slot = drawOrder[i]; + var attachment = slot.attachment; + var slotContainer = this.slotContainers[i]; + if (!(attachment instanceof spine.RegionAttachment)) { + slotContainer.visible = false; + continue; + } + + if (attachment.rendererObject) { + if (!slot.currentSpriteName || slot.currentSpriteName != attachment.name) { + var spriteName = attachment.rendererObject.name; + if (slot.currentSprite !== undefined) { + slot.currentSprite.visible = false; + } + slot.sprites = slot.sprites || {}; + if (slot.sprites[spriteName] !== undefined) { + slot.sprites[spriteName].visible = true; + } else { + var sprite = this.createSprite(slot, attachment.rendererObject); + slotContainer.addChild(sprite); + } + slot.currentSprite = slot.sprites[spriteName]; + slot.currentSpriteName = spriteName; + } + } + slotContainer.visible = true; + + var bone = slot.bone; + + slotContainer.position.x = bone.worldX + attachment.x * bone.m00 + attachment.y * bone.m01; + slotContainer.position.y = bone.worldY + attachment.x * bone.m10 + attachment.y * bone.m11; + slotContainer.scale.x = bone.worldScaleX; + slotContainer.scale.y = bone.worldScaleY; + + slotContainer.rotation = -(slot.bone.worldRotation * Math.PI / 180); + } + + PIXI.DisplayObjectContainer.prototype.updateTransform.call(this); +}; + + +PIXI.Spine.prototype.createSprite = function (slot, descriptor) { + var name = PIXI.TextureCache[descriptor.name] ? descriptor.name : descriptor.name + ".png"; + var sprite = new PIXI.Sprite(PIXI.Texture.fromFrame(name)); + sprite.scale = descriptor.scale; + sprite.rotation = descriptor.rotation; + sprite.anchor.x = sprite.anchor.y = 0.5; + + slot.sprites = slot.sprites || {}; + slot.sprites[descriptor.name] = sprite; + return sprite; +}; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +PIXI.BaseTextureCache = {}; +PIXI.texturesToUpdate = []; +PIXI.texturesToDestroy = []; + +PIXI.BaseTextureCacheIdGenerator = 0; + +/** + * A texture stores the information that represents an image. All textures have a base texture + * + * @class BaseTexture + * @uses EventTarget + * @constructor + * @param source {String} the source object (image or canvas) + * @param scaleMode {Number} Should be one of the PIXI.scaleMode consts + */ +PIXI.BaseTexture = function(source, scaleMode) +{ + PIXI.EventTarget.call( this ); + + /** + * [read-only] The width of the base texture set when the image has loaded + * + * @property width + * @type Number + * @readOnly + */ + this.width = 100; + + /** + * [read-only] The height of the base texture set when the image has loaded + * + * @property height + * @type Number + * @readOnly + */ + this.height = 100; + + /** + * The scale mode to apply when scaling this texture + * @property scaleMode + * @type PIXI.scaleModes + * @default PIXI.scaleModes.LINEAR + */ + this.scaleMode = scaleMode || PIXI.scaleModes.DEFAULT; + + /** + * [read-only] Describes if the base texture has loaded or not + * + * @property hasLoaded + * @type Boolean + * @readOnly + */ + this.hasLoaded = false; + + /** + * The source that is loaded to create the texture + * + * @property source + * @type Image + */ + this.source = source; + + //TODO will be used for futer pixi 1.5... + this.id = PIXI.BaseTextureCacheIdGenerator++; + + // used for webGL + this._glTextures = []; + + if(!source)return; + + if((this.source.complete || this.source.getContext) && this.source.width && this.source.height) + { + this.hasLoaded = true; + this.width = this.source.width; + this.height = this.source.height; + + PIXI.texturesToUpdate.push(this); + } + else + { + + var scope = this; + this.source.onload = function() { + + scope.hasLoaded = true; + scope.width = scope.source.width; + scope.height = scope.source.height; + + // add it to somewhere... + PIXI.texturesToUpdate.push(scope); + scope.dispatchEvent( { type: 'loaded', content: scope } ); + }; + } + + this.imageUrl = null; + this._powerOf2 = false; + + + +}; + +PIXI.BaseTexture.prototype.constructor = PIXI.BaseTexture; + +/** + * Destroys this base texture + * + * @method destroy + */ +PIXI.BaseTexture.prototype.destroy = function() +{ + if(this.imageUrl) + { + delete PIXI.BaseTextureCache[this.imageUrl]; + this.imageUrl = null; + this.source.src = null; + } + this.source = null; + PIXI.texturesToDestroy.push(this); +}; + +/** + * Changes the source image of the texture + * + * @method updateSourceImage + * @param newSrc {String} the path of the image + */ +PIXI.BaseTexture.prototype.updateSourceImage = function(newSrc) +{ + this.hasLoaded = false; + this.source.src = null; + this.source.src = newSrc; +}; + +/** + * Helper function that returns a base texture based on an image url + * If the image is not in the base texture cache it will be created and loaded + * + * @static + * @method fromImage + * @param imageUrl {String} The image url of the texture + * @param crossorigin {Boolean} + * @param scaleMode {Number} Should be one of the PIXI.scaleMode consts + * @return BaseTexture + */ +PIXI.BaseTexture.fromImage = function(imageUrl, crossorigin, scaleMode) +{ + var baseTexture = PIXI.BaseTextureCache[imageUrl]; + + if(crossorigin === undefined && imageUrl.indexOf('data:') === -1) crossorigin = true; + + if(!baseTexture) + { + // new Image() breaks tex loading in some versions of Chrome. + // See https://code.google.com/p/chromium/issues/detail?id=238071 + var image = new Image();//document.createElement('img'); + if (crossorigin) + { + image.crossOrigin = ''; + } + image.src = imageUrl; + baseTexture = new PIXI.BaseTexture(image, scaleMode); + baseTexture.imageUrl = imageUrl; + PIXI.BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; +}; + +/** + * Helper function that returns a base texture based on a canvas element + * If the image is not in the base texture cache it will be created and loaded + * + * @static + * @method fromCanvas + * @param canvas {Canvas} The canvas element source of the texture + * @param scaleMode {Number} Should be one of the PIXI.scaleMode consts + * @return BaseTexture + */ +PIXI.BaseTexture.fromCanvas = function(canvas, scaleMode) +{ + if(!canvas._pixiId) + { + canvas._pixiId = 'canvas_' + PIXI.TextureCacheIdGenerator++; + } + + var baseTexture = PIXI.BaseTextureCache[canvas._pixiId]; + + if(!baseTexture) + { + baseTexture = new PIXI.BaseTexture(canvas, scaleMode); + PIXI.BaseTextureCache[canvas._pixiId] = baseTexture; + } + + return baseTexture; +}; + + + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +PIXI.TextureCache = {}; +PIXI.FrameCache = {}; + +PIXI.TextureCacheIdGenerator = 0; + +/** + * A texture stores the information that represents an image or part of an image. It cannot be added + * to the display list directly. To do this use PIXI.Sprite. If no frame is provided then the whole image is used + * + * @class Texture + * @uses EventTarget + * @constructor + * @param baseTexture {BaseTexture} The base texture source to create the texture from + * @param frame {Rectangle} The rectangle frame of the texture to show + */ +PIXI.Texture = function(baseTexture, frame) +{ + PIXI.EventTarget.call( this ); + + if(!frame) + { + this.noFrame = true; + frame = new PIXI.Rectangle(0,0,1,1); + } + + if(baseTexture instanceof PIXI.Texture) + baseTexture = baseTexture.baseTexture; + + /** + * The base texture of that this texture uses + * + * @property baseTexture + * @type BaseTexture + */ + this.baseTexture = baseTexture; + + /** + * The frame specifies the region of the base texture that this texture uses + * + * @property frame + * @type Rectangle + */ + this.frame = frame; + + /** + * The trim point + * + * @property trim + * @type Rectangle + */ + this.trim = null; + + this.scope = this; + + this._uvs = null; + + if(baseTexture.hasLoaded) + { + if(this.noFrame)frame = new PIXI.Rectangle(0,0, baseTexture.width, baseTexture.height); + + this.setFrame(frame); + } + else + { + var scope = this; + baseTexture.addEventListener('loaded', function(){ scope.onBaseTextureLoaded(); }); + } +}; + +PIXI.Texture.prototype.constructor = PIXI.Texture; + +/** + * Called when the base texture is loaded + * + * @method onBaseTextureLoaded + * @param event + * @private + */ +PIXI.Texture.prototype.onBaseTextureLoaded = function() +{ + var baseTexture = this.baseTexture; + baseTexture.removeEventListener( 'loaded', this.onLoaded ); + + if(this.noFrame)this.frame = new PIXI.Rectangle(0,0, baseTexture.width, baseTexture.height); + + this.setFrame(this.frame); + + this.scope.dispatchEvent( { type: 'update', content: this } ); +}; + +/** + * Destroys this texture + * + * @method destroy + * @param destroyBase {Boolean} Whether to destroy the base texture as well + */ +PIXI.Texture.prototype.destroy = function(destroyBase) +{ + if(destroyBase) this.baseTexture.destroy(); +}; + +/** + * Specifies the rectangle region of the baseTexture + * + * @method setFrame + * @param frame {Rectangle} The frame of the texture to set it to + */ +PIXI.Texture.prototype.setFrame = function(frame) +{ + this.frame = frame; + this.width = frame.width; + this.height = frame.height; + + if(frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) + { + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions ' + this); + } + + this.updateFrame = true; + + PIXI.Texture.frameUpdates.push(this); + + + //this.dispatchEvent( { type: 'update', content: this } ); +}; + +PIXI.Texture.prototype._updateWebGLuvs = function() +{ + if(!this._uvs)this._uvs = new PIXI.TextureUvs(); + + var frame = this.frame; + var tw = this.baseTexture.width; + var th = this.baseTexture.height; + + this._uvs.x0 = frame.x / tw; + this._uvs.y0 = frame.y / th; + + this._uvs.x1 = (frame.x + frame.width) / tw; + this._uvs.y1 = frame.y / th; + + this._uvs.x2 = (frame.x + frame.width) / tw; + this._uvs.y2 = (frame.y + frame.height) / th; + + this._uvs.x3 = frame.x / tw; + this._uvs.y3 = (frame.y + frame.height) / th; +}; + +/** + * Helper function that returns a texture based on an image url + * If the image is not in the texture cache it will be created and loaded + * + * @static + * @method fromImage + * @param imageUrl {String} The image url of the texture + * @param crossorigin {Boolean} Whether requests should be treated as crossorigin + * @param scaleMode {Number} Should be one of the PIXI.scaleMode consts + * @return Texture + */ +PIXI.Texture.fromImage = function(imageUrl, crossorigin, scaleMode) +{ + var texture = PIXI.TextureCache[imageUrl]; + + if(!texture) + { + texture = new PIXI.Texture(PIXI.BaseTexture.fromImage(imageUrl, crossorigin, scaleMode)); + PIXI.TextureCache[imageUrl] = texture; + } + + return texture; +}; + +/** + * Helper function that returns a texture based on a frame id + * If the frame id is not in the texture cache an error will be thrown + * + * @static + * @method fromFrame + * @param frameId {String} The frame id of the texture + * @return Texture + */ +PIXI.Texture.fromFrame = function(frameId) +{ + var texture = PIXI.TextureCache[frameId]; + if(!texture) throw new Error('The frameId "' + frameId + '" does not exist in the texture cache '); + return texture; +}; + +/** + * Helper function that returns a texture based on a canvas element + * If the canvas is not in the texture cache it will be created and loaded + * + * @static + * @method fromCanvas + * @param canvas {Canvas} The canvas element source of the texture + * @param scaleMode {Number} Should be one of the PIXI.scaleMode consts + * @return Texture + */ +PIXI.Texture.fromCanvas = function(canvas, scaleMode) +{ + var baseTexture = PIXI.BaseTexture.fromCanvas(canvas, scaleMode); + + return new PIXI.Texture( baseTexture ); + +}; + + +/** + * Adds a texture to the textureCache. + * + * @static + * @method addTextureToCache + * @param texture {Texture} + * @param id {String} the id that the texture will be stored against. + */ +PIXI.Texture.addTextureToCache = function(texture, id) +{ + PIXI.TextureCache[id] = texture; +}; + +/** + * Remove a texture from the textureCache. + * + * @static + * @method removeTextureFromCache + * @param id {String} the id of the texture to be removed + * @return {Texture} the texture that was removed + */ +PIXI.Texture.removeTextureFromCache = function(id) +{ + var texture = PIXI.TextureCache[id]; + delete PIXI.TextureCache[id]; + delete PIXI.BaseTextureCache[id]; + return texture; +}; + +// this is more for webGL.. it contains updated frames.. +PIXI.Texture.frameUpdates = []; + +PIXI.TextureUvs = function() +{ + this.x0 = 0; + this.y0 = 0; + + this.x1 = 0; + this.y1 = 0; + + this.x2 = 0; + this.y2 = 0; + + this.x3 = 0; + this.y4 = 0; + + +}; + + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +/** + A RenderTexture is a special texture that allows any pixi displayObject to be rendered to it. + + __Hint__: All DisplayObjects (exmpl. Sprites) that render on RenderTexture should be preloaded. + Otherwise black rectangles will be drawn instead. + + RenderTexture takes snapshot of DisplayObject passed to render method. If DisplayObject is passed to render method, position and rotation of it will be ignored. For example: + + var renderTexture = new PIXI.RenderTexture(800, 600); + var sprite = PIXI.Sprite.fromImage("spinObj_01.png"); + sprite.position.x = 800/2; + sprite.position.y = 600/2; + sprite.anchor.x = 0.5; + sprite.anchor.y = 0.5; + renderTexture.render(sprite); + + Sprite in this case will be rendered to 0,0 position. To render this sprite at center DisplayObjectContainer should be used: + + var doc = new PIXI.DisplayObjectContainer(); + doc.addChild(sprite); + renderTexture.render(doc); // Renders to center of renderTexture + + * @class RenderTexture + * @extends Texture + * @constructor + * @param width {Number} The width of the render texture + * @param height {Number} The height of the render texture + * @param scaleMode {Number} Should be one of the PIXI.scaleMode consts + */ +PIXI.RenderTexture = function(width, height, renderer, scaleMode) +{ + PIXI.EventTarget.call( this ); + + /** + * The with of the render texture + * + * @property width + * @type Number + */ + this.width = width || 100; + /** + * The height of the render texture + * + * @property height + * @type Number + */ + this.height = height || 100; + + /** + * The framing rectangle of the render texture + * + * @property frame + * @type Rectangle + */ + this.frame = new PIXI.Rectangle(0, 0, this.width, this.height); + + /** + * The base texture object that this texture uses + * + * @property baseTexture + * @type BaseTexture + */ + this.baseTexture = new PIXI.BaseTexture(); + this.baseTexture.width = this.width; + this.baseTexture.height = this.height; + this.baseTexture._glTextures = []; + + this.baseTexture.scaleMode = scaleMode || PIXI.scaleModes.DEFAULT; + + this.baseTexture.hasLoaded = true; + + // each render texture can only belong to one renderer at the moment if its webGL + this.renderer = renderer || PIXI.defaultRenderer; + + if(this.renderer.type === PIXI.WEBGL_RENDERER) + { + var gl = this.renderer.gl; + + this.textureBuffer = new PIXI.FilterTexture(gl, this.width, this.height, this.baseTexture.scaleMode); + this.baseTexture._glTextures[gl.id] = this.textureBuffer.texture; + + this.render = this.renderWebGL; + this.projection = new PIXI.Point(this.width/2 , -this.height/2); + } + else + { + this.render = this.renderCanvas; + this.textureBuffer = new PIXI.CanvasBuffer(this.width, this.height); + this.baseTexture.source = this.textureBuffer.canvas; + } + + PIXI.Texture.frameUpdates.push(this); + + +}; + +PIXI.RenderTexture.prototype = Object.create(PIXI.Texture.prototype); +PIXI.RenderTexture.prototype.constructor = PIXI.RenderTexture; + +PIXI.RenderTexture.prototype.resize = function(width, height) +{ + this.width = width; + this.height = height; + + this.frame.width = this.width; + this.frame.height = this.height; + + if(this.renderer.type === PIXI.WEBGL_RENDERER) + { + this.projection.x = this.width / 2; + this.projection.y = -this.height / 2; + + var gl = this.renderer.gl; + gl.bindTexture(gl.TEXTURE_2D, this.baseTexture._glTextures[gl.id]); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.width, this.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + } + else + { + this.textureBuffer.resize(this.width, this.height); + } + + PIXI.Texture.frameUpdates.push(this); +}; + +/** + * This function will draw the display object to the texture. + * + * @method renderWebGL + * @param displayObject {DisplayObject} The display object to render this texture on + * @param clear {Boolean} If true the texture will be cleared before the displayObject is drawn + * @private + */ +PIXI.RenderTexture.prototype.renderWebGL = function(displayObject, position, clear) +{ + //TOOD replace position with matrix.. + var gl = this.renderer.gl; + + gl.colorMask(true, true, true, true); + + gl.viewport(0, 0, this.width, this.height); + + gl.bindFramebuffer(gl.FRAMEBUFFER, this.textureBuffer.frameBuffer ); + + if(clear)this.textureBuffer.clear(); + + // THIS WILL MESS WITH HIT TESTING! + var children = displayObject.children; + + //TODO -? create a new one??? dont think so! + var originalWorldTransform = displayObject.worldTransform; + displayObject.worldTransform = PIXI.RenderTexture.tempMatrix; + // modify to flip... + displayObject.worldTransform.d = -1; + displayObject.worldTransform.ty = this.projection.y * -2; + + if(position) + { + displayObject.worldTransform.tx = position.x; + displayObject.worldTransform.ty -= position.y; + } + + for(var i=0,j=children.length; i} assetURLs an array of image/sprite sheet urls that you would like loaded + * supported. Supported image formats include 'jpeg', 'jpg', 'png', 'gif'. Supported + * sprite sheet data formats only include 'JSON' at this time. Supported bitmap font + * data formats include 'xml' and 'fnt'. + * @param crossorigin {Boolean} Whether requests should be treated as crossorigin + */ +PIXI.AssetLoader = function(assetURLs, crossorigin) +{ + PIXI.EventTarget.call(this); + + /** + * The array of asset URLs that are going to be loaded + * + * @property assetURLs + * @type Array + */ + this.assetURLs = assetURLs; + + /** + * Whether the requests should be treated as cross origin + * + * @property crossorigin + * @type Boolean + */ + this.crossorigin = crossorigin; + + /** + * Maps file extension to loader types + * + * @property loadersByType + * @type Object + */ + this.loadersByType = { + 'jpg': PIXI.ImageLoader, + 'jpeg': PIXI.ImageLoader, + 'png': PIXI.ImageLoader, + 'gif': PIXI.ImageLoader, + 'json': PIXI.JsonLoader, + 'atlas': PIXI.AtlasLoader, + 'anim': PIXI.SpineLoader, + 'xml': PIXI.BitmapFontLoader, + 'fnt': PIXI.BitmapFontLoader + }; +}; + +/** + * Fired when an item has loaded + * @event onProgress + */ + +/** + * Fired when all the assets have loaded + * @event onComplete + */ + +// constructor +PIXI.AssetLoader.prototype.constructor = PIXI.AssetLoader; + +/** + * Given a filename, returns its extension, wil + * + * @method _getDataType + * @param str {String} the name of the asset + */ +PIXI.AssetLoader.prototype._getDataType = function(str) +{ + var test = 'data:'; + //starts with 'data:' + var start = str.slice(0, test.length).toLowerCase(); + if (start === test) { + var data = str.slice(test.length); + + var sepIdx = data.indexOf(','); + if (sepIdx === -1) //malformed data URI scheme + return null; + + //e.g. 'image/gif;base64' => 'image/gif' + var info = data.slice(0, sepIdx).split(';')[0]; + + //We might need to handle some special cases here... + //standardize text/plain to 'txt' file extension + if (!info || info.toLowerCase() === 'text/plain') + return 'txt'; + + //User specified mime type, try splitting it by '/' + return info.split('/').pop().toLowerCase(); + } + + return null; +}; + +/** + * Starts loading the assets sequentially + * + * @method load + */ +PIXI.AssetLoader.prototype.load = function() +{ + var scope = this; + + function onLoad(evt) { + scope.onAssetLoaded(evt.content); + } + + this.loadCount = this.assetURLs.length; + + for (var i=0; i < this.assetURLs.length; i++) + { + var fileName = this.assetURLs[i]; + //first see if we have a data URI scheme.. + var fileType = this._getDataType(fileName); + + //if not, assume it's a file URI + if (!fileType) + fileType = fileName.split('?').shift().split('.').pop().toLowerCase(); + + var Constructor = this.loadersByType[fileType]; + if(!Constructor) + throw new Error(fileType + ' is an unsupported file type'); + + var loader = new Constructor(fileName, this.crossorigin); + + loader.addEventListener('loaded', onLoad); + loader.load(); + } +}; + +/** + * Invoked after each file is loaded + * + * @method onAssetLoaded + * @private + */ +PIXI.AssetLoader.prototype.onAssetLoaded = function(loader) +{ + this.loadCount--; + this.dispatchEvent({ type: 'onProgress', content: this, loader: loader }); + if (this.onProgress) this.onProgress(loader); + + if (!this.loadCount) + { + this.dispatchEvent({type: 'onComplete', content: this}); + if(this.onComplete) this.onComplete(); + } +}; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +/** + * The json file loader is used to load in JSON data and parse it + * When loaded this class will dispatch a 'loaded' event + * If loading fails this class will dispatch an 'error' event + * + * @class JsonLoader + * @uses EventTarget + * @constructor + * @param url {String} The url of the JSON file + * @param crossorigin {Boolean} Whether requests should be treated as crossorigin + */ +PIXI.JsonLoader = function (url, crossorigin) { + PIXI.EventTarget.call(this); + + /** + * The url of the bitmap font data + * + * @property url + * @type String + */ + this.url = url; + + /** + * Whether the requests should be treated as cross origin + * + * @property crossorigin + * @type Boolean + */ + this.crossorigin = crossorigin; + + /** + * [read-only] The base url of the bitmap font data + * + * @property baseUrl + * @type String + * @readOnly + */ + this.baseUrl = url.replace(/[^\/]*$/, ''); + + /** + * [read-only] Whether the data has loaded yet + * + * @property loaded + * @type Boolean + * @readOnly + */ + this.loaded = false; + +}; + +// constructor +PIXI.JsonLoader.prototype.constructor = PIXI.JsonLoader; + +/** + * Loads the JSON data + * + * @method load + */ +PIXI.JsonLoader.prototype.load = function () { + + var scope = this; + + if(window.XDomainRequest) + { + this.ajaxRequest = new window.XDomainRequest(); + + // XDomainRequest has a few querks. Occasionally it will abort requests + // A way to avoid this is to make sure ALL callbacks are set even if not used + // More info here: http://stackoverflow.com/questions/15786966/xdomainrequest-aborts-post-on-ie-9 + this.ajaxRequest.timeout = 3000; + + this.ajaxRequest.onerror = function () { + scope.onError(); + }; + + this.ajaxRequest.ontimeout = function () { + scope.onError(); + }; + + this.ajaxRequest.onprogress = function() {}; + + } + else if (window.XMLHttpRequest) + { + this.ajaxRequest = new window.XMLHttpRequest(); + } + else + { + this.ajaxRequest = new window.ActiveXObject('Microsoft.XMLHTTP'); + } + + + + this.ajaxRequest.onload = function(){ + + scope.onJSONLoaded(); + }; + + this.ajaxRequest.open('GET',this.url,true); + + this.ajaxRequest.send(); +}; + +/** + * Invoke when JSON file is loaded + * + * @method onJSONLoaded + * @private + */ +PIXI.JsonLoader.prototype.onJSONLoaded = function () { + + if(!this.ajaxRequest.responseText ) + { + this.onError(); + return; + } + + this.json = JSON.parse(this.ajaxRequest.responseText); + + if(this.json.frames) + { + // sprite sheet + var scope = this; + var textureUrl = this.baseUrl + this.json.meta.image; + var image = new PIXI.ImageLoader(textureUrl, this.crossorigin); + var frameData = this.json.frames; + + this.texture = image.texture.baseTexture; + image.addEventListener('loaded', function() { + scope.onLoaded(); + }); + + for (var i in frameData) { + var rect = frameData[i].frame; + if (rect) { + PIXI.TextureCache[i] = new PIXI.Texture(this.texture, { + x: rect.x, + y: rect.y, + width: rect.w, + height: rect.h + }); + + // check to see ifthe sprite ha been trimmed.. + if (frameData[i].trimmed) { + + var texture = PIXI.TextureCache[i]; + + var actualSize = frameData[i].sourceSize; + var realSize = frameData[i].spriteSourceSize; + + texture.trim = new PIXI.Rectangle(realSize.x, realSize.y, actualSize.w, actualSize.h); + } + } + } + + image.load(); + + } + else if(this.json.bones) + { + // spine animation + var spineJsonParser = new spine.SkeletonJson(); + var skeletonData = spineJsonParser.readSkeletonData(this.json); + PIXI.AnimCache[this.url] = skeletonData; + this.onLoaded(); + } + else + { + this.onLoaded(); + } +}; + +/** + * Invoke when json file loaded + * + * @method onLoaded + * @private + */ +PIXI.JsonLoader.prototype.onLoaded = function () { + this.loaded = true; + this.dispatchEvent({ + type: 'loaded', + content: this + }); +}; + +/** + * Invoke when error occured + * + * @method onError + * @private + */ +PIXI.JsonLoader.prototype.onError = function () { + + this.dispatchEvent({ + type: 'error', + content: this + }); +}; +/** + * @author Martin Kelm http://mkelm.github.com + */ + +/** + * The atlas file loader is used to load in Atlas data and parse it + * When loaded this class will dispatch a 'loaded' event + * If loading fails this class will dispatch an 'error' event + * @class AtlasLoader + * @extends EventTarget + * @constructor + * @param {String} url the url of the JSON file + * @param {Boolean} crossorigin + */ + +PIXI.AtlasLoader = function (url, crossorigin) { + PIXI.EventTarget.call(this); + this.url = url; + this.baseUrl = url.replace(/[^\/]*$/, ''); + this.crossorigin = crossorigin; + this.loaded = false; + +}; + +// constructor +PIXI.AtlasLoader.constructor = PIXI.AtlasLoader; + + + /** + * Starts loading the JSON file + * + * @method load + */ +PIXI.AtlasLoader.prototype.load = function () { + this.ajaxRequest = new PIXI.AjaxRequest(); + this.ajaxRequest.onreadystatechange = this.onAtlasLoaded.bind(this); + + this.ajaxRequest.open('GET', this.url, true); + if (this.ajaxRequest.overrideMimeType) this.ajaxRequest.overrideMimeType('application/json'); + this.ajaxRequest.send(null); +}; + +/** + * Invoke when JSON file is loaded + * @method onAtlasLoaded + * @private + */ +PIXI.AtlasLoader.prototype.onAtlasLoaded = function () { + if (this.ajaxRequest.readyState === 4) { + if (this.ajaxRequest.status === 200 || window.location.href.indexOf('http') === -1) { + this.atlas = { + meta : { + image : [] + }, + frames : [] + }; + var result = this.ajaxRequest.responseText.split(/\r?\n/); + var lineCount = -3; + + var currentImageId = 0; + var currentFrame = null; + var nameInNextLine = false; + + var i = 0, + j = 0, + selfOnLoaded = this.onLoaded.bind(this); + + // parser without rotation support yet! + for (i = 0; i < result.length; i++) { + result[i] = result[i].replace(/^\s+|\s+$/g, ''); + if (result[i] === '') { + nameInNextLine = i+1; + } + if (result[i].length > 0) { + if (nameInNextLine === i) { + this.atlas.meta.image.push(result[i]); + currentImageId = this.atlas.meta.image.length - 1; + this.atlas.frames.push({}); + lineCount = -3; + } else if (lineCount > 0) { + if (lineCount % 7 === 1) { // frame name + if (currentFrame != null) { //jshint ignore:line + this.atlas.frames[currentImageId][currentFrame.name] = currentFrame; + } + currentFrame = { name: result[i], frame : {} }; + } else { + var text = result[i].split(' '); + if (lineCount % 7 === 3) { // position + currentFrame.frame.x = Number(text[1].replace(',', '')); + currentFrame.frame.y = Number(text[2]); + } else if (lineCount % 7 === 4) { // size + currentFrame.frame.w = Number(text[1].replace(',', '')); + currentFrame.frame.h = Number(text[2]); + } else if (lineCount % 7 === 5) { // real size + var realSize = { + x : 0, + y : 0, + w : Number(text[1].replace(',', '')), + h : Number(text[2]) + }; + + if (realSize.w > currentFrame.frame.w || realSize.h > currentFrame.frame.h) { + currentFrame.trimmed = true; + currentFrame.realSize = realSize; + } else { + currentFrame.trimmed = false; + } + } + } + } + lineCount++; + } + } + + if (currentFrame != null) { //jshint ignore:line + this.atlas.frames[currentImageId][currentFrame.name] = currentFrame; + } + + if (this.atlas.meta.image.length > 0) { + this.images = []; + for (j = 0; j < this.atlas.meta.image.length; j++) { + // sprite sheet + var textureUrl = this.baseUrl + this.atlas.meta.image[j]; + var frameData = this.atlas.frames[j]; + this.images.push(new PIXI.ImageLoader(textureUrl, this.crossorigin)); + + for (i in frameData) { + var rect = frameData[i].frame; + if (rect) { + PIXI.TextureCache[i] = new PIXI.Texture(this.images[j].texture.baseTexture, { + x: rect.x, + y: rect.y, + width: rect.w, + height: rect.h + }); + if (frameData[i].trimmed) { + PIXI.TextureCache[i].realSize = frameData[i].realSize; + // trim in pixi not supported yet, todo update trim properties if it is done ... + PIXI.TextureCache[i].trim.x = 0; + PIXI.TextureCache[i].trim.y = 0; + } + } + } + } + + this.currentImageId = 0; + for (j = 0; j < this.images.length; j++) { + this.images[j].addEventListener('loaded', selfOnLoaded); + } + this.images[this.currentImageId].load(); + + } else { + this.onLoaded(); + } + + } else { + this.onError(); + } + } +}; + +/** + * Invoke when json file has loaded + * @method onLoaded + * @private + */ +PIXI.AtlasLoader.prototype.onLoaded = function () { + if (this.images.length - 1 > this.currentImageId) { + this.currentImageId++; + this.images[this.currentImageId].load(); + } else { + this.loaded = true; + this.dispatchEvent({ + type: 'loaded', + content: this + }); + } +}; + +/** + * Invoke when error occured + * @method onError + * @private + */ +PIXI.AtlasLoader.prototype.onError = function () { + this.dispatchEvent({ + type: 'error', + content: this + }); +}; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +/** + * The sprite sheet loader is used to load in JSON sprite sheet data + * To generate the data you can use http://www.codeandweb.com/texturepacker and publish in the 'JSON' format + * There is a free version so thats nice, although the paid version is great value for money. + * It is highly recommended to use Sprite sheets (also know as a 'texture atlas') as it means sprites can be batched and drawn together for highly increased rendering speed. + * Once the data has been loaded the frames are stored in the PIXI texture cache and can be accessed though PIXI.Texture.fromFrameId() and PIXI.Sprite.fromFrameId() + * This loader will load the image file that the Spritesheet points to as well as the data. + * When loaded this class will dispatch a 'loaded' event + * + * @class SpriteSheetLoader + * @uses EventTarget + * @constructor + * @param url {String} The url of the sprite sheet JSON file + * @param crossorigin {Boolean} Whether requests should be treated as crossorigin + */ +PIXI.SpriteSheetLoader = function (url, crossorigin) { + /* + * i use texture packer to load the assets.. + * http://www.codeandweb.com/texturepacker + * make sure to set the format as 'JSON' + */ + PIXI.EventTarget.call(this); + + /** + * The url of the bitmap font data + * + * @property url + * @type String + */ + this.url = url; + + /** + * Whether the requests should be treated as cross origin + * + * @property crossorigin + * @type Boolean + */ + this.crossorigin = crossorigin; + + /** + * [read-only] The base url of the bitmap font data + * + * @property baseUrl + * @type String + * @readOnly + */ + this.baseUrl = url.replace(/[^\/]*$/, ''); + + /** + * The texture being loaded + * + * @property texture + * @type Texture + */ + this.texture = null; + + /** + * The frames of the sprite sheet + * + * @property frames + * @type Object + */ + this.frames = {}; +}; + +// constructor +PIXI.SpriteSheetLoader.prototype.constructor = PIXI.SpriteSheetLoader; + +/** + * This will begin loading the JSON file + * + * @method load + */ +PIXI.SpriteSheetLoader.prototype.load = function () { + var scope = this; + var jsonLoader = new PIXI.JsonLoader(this.url, this.crossorigin); + jsonLoader.addEventListener('loaded', function (event) { + scope.json = event.content.json; + scope.onLoaded(); + }); + jsonLoader.load(); +}; + +/** + * Invoke when all files are loaded (json and texture) + * + * @method onLoaded + * @private + */ +PIXI.SpriteSheetLoader.prototype.onLoaded = function () { + this.dispatchEvent({ + type: 'loaded', + content: this + }); +}; + +/** + * @author Mat Groves http://matgroves.com/ @Doormat23 + */ + +/** + * The image loader class is responsible for loading images file formats ('jpeg', 'jpg', 'png' and 'gif') + * Once the image has been loaded it is stored in the PIXI texture cache and can be accessed though PIXI.Texture.fromFrameId() and PIXI.Sprite.fromFrameId() + * When loaded this class will dispatch a 'loaded' event + * + * @class ImageLoader + * @uses EventTarget + * @constructor + * @param url {String} The url of the image + * @param crossorigin {Boolean} Whether requests should be treated as crossorigin + */ +PIXI.ImageLoader = function(url, crossorigin) +{ + PIXI.EventTarget.call(this); + + /** + * The texture being loaded + * + * @property texture + * @type Texture + */ + this.texture = PIXI.Texture.fromImage(url, crossorigin); + + /** + * if the image is loaded with loadFramedSpriteSheet + * frames will contain the sprite sheet frames + * + */ + this.frames = []; +}; + +// constructor +PIXI.ImageLoader.prototype.constructor = PIXI.ImageLoader; + +/** + * Loads image or takes it from cache + * + * @method load + */ +PIXI.ImageLoader.prototype.load = function() +{ + if(!this.texture.baseTexture.hasLoaded) + { + var scope = this; + this.texture.baseTexture.addEventListener('loaded', function() + { + scope.onLoaded(); + }); + } + else + { + this.onLoaded(); + } +}; + +/** + * Invoked when image file is loaded or it is already cached and ready to use + * + * @method onLoaded + * @private + */ +PIXI.ImageLoader.prototype.onLoaded = function() +{ + this.dispatchEvent({type: 'loaded', content: this}); +}; + +/** + * Loads image and split it to uniform sized frames + * + * + * @method loadFramedSpriteSheet + * @param frameWidth {Number} width of each frame + * @param frameHeight {Number} height of each frame + * @param textureName {String} if given, the frames will be cached in - format + */ +PIXI.ImageLoader.prototype.loadFramedSpriteSheet = function(frameWidth, frameHeight, textureName) +{ + this.frames = []; + var cols = Math.floor(this.texture.width / frameWidth); + var rows = Math.floor(this.texture.height / frameHeight); + + var i=0; + for (var y=0; y>16)/255,(255&a>>8)/255,(255&a)/255]}function d(a){return[(255&a>>16)/255,(255&a>>8)/255,(255&a)/255]}var e=this,f=f||{};f.Point=function(a,b){this.x=a||0,this.y=b||0},f.Point.prototype.clone=function(){return new f.Point(this.x,this.y)},f.Point.prototype.constructor=f.Point,f.Rectangle=function(a,b,c,d){this.x=a||0,this.y=b||0,this.width=c||0,this.height=d||0},f.Rectangle.prototype.clone=function(){return new f.Rectangle(this.x,this.y,this.width,this.height)},f.Rectangle.prototype.contains=function(a,b){if(this.width<=0||this.height<=0)return!1;var c=this.x;if(a>=c&&a<=c+this.width){var d=this.y;if(b>=d&&b<=d+this.height)return!0}return!1},f.Rectangle.prototype.constructor=f.Rectangle,f.Polygon=function(a){if(a instanceof Array||(a=Array.prototype.slice.call(arguments)),"number"==typeof a[0]){for(var b=[],c=0,d=a.length;d>c;c+=2)b.push(new f.Point(a[c],a[c+1]));a=b}this.points=a},f.Polygon.prototype.clone=function(){for(var a=[],b=0;bb!=i>b&&(h-f)*(b-g)/(i-g)+f>a;j&&(c=!c)}return c},f.Polygon.prototype.constructor=f.Polygon,f.Circle=function(a,b,c){this.x=a||0,this.y=b||0,this.radius=c||0},f.Circle.prototype.clone=function(){return new f.Circle(this.x,this.y,this.radius)},f.Circle.prototype.contains=function(a,b){if(this.radius<=0)return!1;var c=this.x-a,d=this.y-b,e=this.radius*this.radius;return c*=c,d*=d,e>=c+d},f.Circle.prototype.constructor=f.Circle,f.Ellipse=function(a,b,c,d){this.x=a||0,this.y=b||0,this.width=c||0,this.height=d||0},f.Ellipse.prototype.clone=function(){return new f.Ellipse(this.x,this.y,this.width,this.height)},f.Ellipse.prototype.contains=function(a,b){if(this.width<=0||this.height<=0)return!1;var c=(a-this.x)/this.width-.5,d=(b-this.y)/this.height-.5;return c*=c,d*=d,.25>c+d},f.Ellipse.getBounds=function(){return new f.Rectangle(this.x,this.y,this.width,this.height)},f.Ellipse.prototype.constructor=f.Ellipse,c(),f.mat3={},f.mat3.create=function(){var a=new f.Matrix(9);return a[0]=1,a[1]=0,a[2]=0,a[3]=0,a[4]=1,a[5]=0,a[6]=0,a[7]=0,a[8]=1,a},f.mat3.identity=function(a){return a[0]=1,a[1]=0,a[2]=0,a[3]=0,a[4]=1,a[5]=0,a[6]=0,a[7]=0,a[8]=1,a},f.mat4={},f.mat4.create=function(){var a=new f.Matrix(16);return a[0]=1,a[1]=0,a[2]=0,a[3]=0,a[4]=0,a[5]=1,a[6]=0,a[7]=0,a[8]=0,a[9]=0,a[10]=1,a[11]=0,a[12]=0,a[13]=0,a[14]=0,a[15]=1,a},f.mat3.multiply=function(a,b,c){c||(c=a);var d=a[0],e=a[1],f=a[2],g=a[3],h=a[4],i=a[5],j=a[6],k=a[7],l=a[8],m=b[0],n=b[1],o=b[2],p=b[3],q=b[4],r=b[5],s=b[6],t=b[7],u=b[8];return c[0]=m*d+n*g+o*j,c[1]=m*e+n*h+o*k,c[2]=m*f+n*i+o*l,c[3]=p*d+q*g+r*j,c[4]=p*e+q*h+r*k,c[5]=p*f+q*i+r*l,c[6]=s*d+t*g+u*j,c[7]=s*e+t*h+u*k,c[8]=s*f+t*i+u*l,c},f.mat3.clone=function(a){var b=new f.Matrix(9);return b[0]=a[0],b[1]=a[1],b[2]=a[2],b[3]=a[3],b[4]=a[4],b[5]=a[5],b[6]=a[6],b[7]=a[7],b[8]=a[8],b},f.mat3.transpose=function(a,b){if(!b||a===b){var c=a[1],d=a[2],e=a[5];return a[1]=a[3],a[2]=a[6],a[3]=c,a[5]=a[7],a[6]=d,a[7]=e,a}return b[0]=a[0],b[1]=a[3],b[2]=a[6],b[3]=a[1],b[4]=a[4],b[5]=a[7],b[6]=a[2],b[7]=a[5],b[8]=a[8],b},f.mat3.toMat4=function(a,b){return b||(b=f.mat4.create()),b[15]=1,b[14]=0,b[13]=0,b[12]=0,b[11]=0,b[10]=a[8],b[9]=a[7],b[8]=a[6],b[7]=0,b[6]=a[5],b[5]=a[4],b[4]=a[3],b[3]=0,b[2]=a[2],b[1]=a[1],b[0]=a[0],b},f.mat4.create=function(){var a=new f.Matrix(16);return a[0]=1,a[1]=0,a[2]=0,a[3]=0,a[4]=0,a[5]=1,a[6]=0,a[7]=0,a[8]=0,a[9]=0,a[10]=1,a[11]=0,a[12]=0,a[13]=0,a[14]=0,a[15]=1,a},f.mat4.transpose=function(a,b){if(!b||a===b){var c=a[1],d=a[2],e=a[3],f=a[6],g=a[7],h=a[11];return a[1]=a[4],a[2]=a[8],a[3]=a[12],a[4]=c,a[6]=a[9],a[7]=a[13],a[8]=d,a[9]=f,a[11]=a[14],a[12]=e,a[13]=g,a[14]=h,a}return b[0]=a[0],b[1]=a[4],b[2]=a[8],b[3]=a[12],b[4]=a[1],b[5]=a[5],b[6]=a[9],b[7]=a[13],b[8]=a[2],b[9]=a[6],b[10]=a[10],b[11]=a[14],b[12]=a[3],b[13]=a[7],b[14]=a[11],b[15]=a[15],b},f.mat4.multiply=function(a,b,c){c||(c=a);var d=a[0],e=a[1],f=a[2],g=a[3],h=a[4],i=a[5],j=a[6],k=a[7],l=a[8],m=a[9],n=a[10],o=a[11],p=a[12],q=a[13],r=a[14],s=a[15],t=b[0],u=b[1],v=b[2],w=b[3];return c[0]=t*d+u*h+v*l+w*p,c[1]=t*e+u*i+v*m+w*q,c[2]=t*f+u*j+v*n+w*r,c[3]=t*g+u*k+v*o+w*s,t=b[4],u=b[5],v=b[6],w=b[7],c[4]=t*d+u*h+v*l+w*p,c[5]=t*e+u*i+v*m+w*q,c[6]=t*f+u*j+v*n+w*r,c[7]=t*g+u*k+v*o+w*s,t=b[8],u=b[9],v=b[10],w=b[11],c[8]=t*d+u*h+v*l+w*p,c[9]=t*e+u*i+v*m+w*q,c[10]=t*f+u*j+v*n+w*r,c[11]=t*g+u*k+v*o+w*s,t=b[12],u=b[13],v=b[14],w=b[15],c[12]=t*d+u*h+v*l+w*p,c[13]=t*e+u*i+v*m+w*q,c[14]=t*f+u*j+v*n+w*r,c[15]=t*g+u*k+v*o+w*s,c},f.DisplayObject=function(){this.last=this,this.first=this,this.position=new f.Point,this.scale=new f.Point(1,1),this.pivot=new f.Point(0,0),this.rotation=0,this.alpha=1,this.visible=!0,this.hitArea=null,this.buttonMode=!1,this.renderable=!1,this.parent=null,this.stage=null,this.worldAlpha=1,this._interactive=!1,this.worldTransform=f.mat3.create(),this.localTransform=f.mat3.create(),this.color=[],this.dynamic=!0,this._sr=0,this._cr=1},f.DisplayObject.prototype.constructor=f.DisplayObject,f.DisplayObject.prototype.setInteractive=function(a){this.interactive=a},Object.defineProperty(f.DisplayObject.prototype,"interactive",{get:function(){return this._interactive},set:function(a){this._interactive=a,this.stage&&(this.stage.dirty=!0)}}),Object.defineProperty(f.DisplayObject.prototype,"mask",{get:function(){return this._mask},set:function(a){this._mask=a,a?this.addFilter(a):this.removeFilter()}}),f.DisplayObject.prototype.addFilter=function(a){if(!this.filter){this.filter=!0;var b=new f.FilterBlock,c=new f.FilterBlock;b.mask=a,c.mask=a,b.first=b.last=this,c.first=c.last=this,b.open=!0;var d,e,g=b,h=b;e=this.first._iPrev,e?(d=e._iNext,g._iPrev=e,e._iNext=g):d=this,d&&(d._iPrev=h,h._iNext=d);var g=c,h=c,d=null,e=null;e=this.last,d=e._iNext,d&&(d._iPrev=h,h._iNext=d),g._iPrev=e,e._iNext=g;for(var i=this,j=this.last;i;)i.last==j&&(i.last=c),i=i.parent;this.first=b,this.__renderGroup&&this.__renderGroup.addFilterBlocks(b,c),a.renderable=!1}},f.DisplayObject.prototype.removeFilter=function(){if(this.filter){this.filter=!1;var a=this.first,b=a._iNext,c=a._iPrev;b&&(b._iPrev=c),c&&(c._iNext=b),this.first=a._iNext;var d=this.last,b=d._iNext,c=d._iPrev;b&&(b._iPrev=c),c._iNext=b;for(var e=d._iPrev,f=this;f.last==d&&(f.last=e,f=f.parent););var g=a.mask;g.renderable=!0,this.__renderGroup&&this.__renderGroup.removeFilterBlocks(a,d)}},f.DisplayObject.prototype.updateTransform=function(){this.rotation!==this.rotationCache&&(this.rotationCache=this.rotation,this._sr=Math.sin(this.rotation),this._cr=Math.cos(this.rotation));var a=this.localTransform,b=this.parent.worldTransform,c=this.worldTransform;a[0]=this._cr*this.scale.x,a[1]=-this._sr*this.scale.y,a[3]=this._sr*this.scale.x,a[4]=this._cr*this.scale.y;var d=this.pivot.x,e=this.pivot.y,g=a[0],h=a[1],i=this.position.x-a[0]*d-e*a[1],j=a[3],k=a[4],l=this.position.y-a[4]*e-d*a[3],m=b[0],n=b[1],o=b[2],p=b[3],q=b[4],r=b[5];a[2]=i,a[5]=l,c[0]=m*g+n*j,c[1]=m*h+n*k,c[2]=m*i+n*l+o,c[3]=p*g+q*j,c[4]=p*h+q*k,c[5]=p*i+q*l+r,this.worldAlpha=this.alpha*this.parent.worldAlpha,this.vcount=f.visibleCount},f.visibleCount=0,f.DisplayObjectContainer=function(){f.DisplayObject.call(this),this.children=[]},f.DisplayObjectContainer.prototype=Object.create(f.DisplayObject.prototype),f.DisplayObjectContainer.prototype.constructor=f.DisplayObjectContainer,f.DisplayObjectContainer.prototype.addChild=function(a){if(void 0!=a.parent&&a.parent.removeChild(a),a.parent=this,this.children.push(a),this.stage){var b=a;do b.interactive&&(this.stage.dirty=!0),b.stage=this.stage,b=b._iNext;while(b)}var c,d,e=a.first,f=a.last;d=this.filter?this.last._iPrev:this.last,c=d._iNext;for(var g=this,h=d;g;)g.last==h&&(g.last=a.last),g=g.parent;c&&(c._iPrev=f,f._iNext=c),e._iPrev=d,d._iNext=e,this.__renderGroup&&(a.__renderGroup&&a.__renderGroup.removeDisplayObjectAndChildren(a),this.__renderGroup.addDisplayObjectAndChildren(a))},f.DisplayObjectContainer.prototype.addChildAt=function(a,b){if(!(b>=0&&b<=this.children.length))throw new Error(a+" The index "+b+" supplied is out of bounds "+this.children.length);if(void 0!=a.parent&&a.parent.removeChild(a),a.parent=this,this.stage){var c=a;do c.interactive&&(this.stage.dirty=!0),c.stage=this.stage,c=c._iNext;while(c)}var d,e,f=a.first,g=a.last;if(b==this.children.length){e=this.last;for(var h=this,i=this.last;h;)h.last==i&&(h.last=a.last),h=h.parent}else e=0==b?this:this.children[b-1].last;d=e._iNext,d&&(d._iPrev=g,g._iNext=d),f._iPrev=e,e._iNext=f,this.children.splice(b,0,a),this.__renderGroup&&(a.__renderGroup&&a.__renderGroup.removeDisplayObjectAndChildren(a),this.__renderGroup.addDisplayObjectAndChildren(a))},f.DisplayObjectContainer.prototype.swapChildren=function(){},f.DisplayObjectContainer.prototype.getChildAt=function(a){if(a>=0&&aa;a++)this.children[a].updateTransform()}},f.blendModes={},f.blendModes.NORMAL=0,f.blendModes.SCREEN=1,f.Sprite=function(a){f.DisplayObjectContainer.call(this),this.anchor=new f.Point,this.texture=a,this.blendMode=f.blendModes.NORMAL,this._width=0,this._height=0,a.baseTexture.hasLoaded?this.updateFrame=!0:(this.onTextureUpdateBind=this.onTextureUpdate.bind(this),this.texture.addEventListener("update",this.onTextureUpdateBind)),this.renderable=!0},f.Sprite.prototype=Object.create(f.DisplayObjectContainer.prototype),f.Sprite.prototype.constructor=f.Sprite,Object.defineProperty(f.Sprite.prototype,"width",{get:function(){return this.scale.x*this.texture.frame.width},set:function(a){this.scale.x=a/this.texture.frame.width,this._width=a}}),Object.defineProperty(f.Sprite.prototype,"height",{get:function(){return this.scale.y*this.texture.frame.height},set:function(a){this.scale.y=a/this.texture.frame.height,this._height=a}}),f.Sprite.prototype.setTexture=function(a){this.texture.baseTexture!=a.baseTexture?(this.textureChange=!0,this.texture=a,this.__renderGroup&&this.__renderGroup.updateTexture(this)):this.texture=a,this.updateFrame=!0},f.Sprite.prototype.onTextureUpdate=function(){this._width&&(this.scale.x=this._width/this.texture.frame.width),this._height&&(this.scale.y=this._height/this.texture.frame.height),this.updateFrame=!0},f.Sprite.fromFrame=function(a){var b=f.TextureCache[a];if(!b)throw new Error("The frameId '"+a+"' does not exist in the texture cache"+this);return new f.Sprite(b)},f.Sprite.fromImage=function(a){var b=f.Texture.fromImage(a);return new f.Sprite(b)},f.MovieClip=function(a){f.Sprite.call(this,a[0]),this.textures=a,this.animationSpeed=1,this.loop=!0,this.onComplete=null,this.currentFrame=0,this.playing=!1},f.MovieClip.prototype=Object.create(f.Sprite.prototype),f.MovieClip.prototype.constructor=f.MovieClip,f.MovieClip.prototype.stop=function(){this.playing=!1},f.MovieClip.prototype.play=function(){this.playing=!0},f.MovieClip.prototype.gotoAndStop=function(a){this.playing=!1,this.currentFrame=a;var b=0|this.currentFrame+.5;this.setTexture(this.textures[b%this.textures.length])},f.MovieClip.prototype.gotoAndPlay=function(a){this.currentFrame=a,this.playing=!0},f.MovieClip.prototype.updateTransform=function(){if(f.Sprite.prototype.updateTransform.call(this),this.playing){this.currentFrame+=this.animationSpeed;var a=0|this.currentFrame+.5;this.loop||a=this.textures.length&&(this.gotoAndStop(this.textures.length-1),this.onComplete&&this.onComplete())}},f.FilterBlock=function(a){this.graphics=a,this.visible=!0,this.renderable=!0},f.Text=function(a,b){this.canvas=document.createElement("canvas"),this.context=this.canvas.getContext("2d"),f.Sprite.call(this,f.Texture.fromCanvas(this.canvas)),this.setText(a),this.setStyle(b),this.updateText(),this.dirty=!1},f.Text.prototype=Object.create(f.Sprite.prototype),f.Text.prototype.constructor=f.Text,f.Text.prototype.setStyle=function(a){a=a||{},a.font=a.font||"bold 20pt Arial",a.fill=a.fill||"black",a.align=a.align||"left",a.stroke=a.stroke||"black",a.strokeThickness=a.strokeThickness||0,a.wordWrap=a.wordWrap||!1,a.wordWrapWidth=a.wordWrapWidth||100,this.style=a,this.dirty=!0},f.Sprite.prototype.setText=function(a){this.text=a.toString()||" ",this.dirty=!0},f.Text.prototype.updateText=function(){this.context.font=this.style.font;var a=this.text;this.style.wordWrap&&(a=this.wordWrap(this.text));for(var b=a.split(/(?:\r\n|\r|\n)/),c=[],d=0,e=0;ee?f:arguments.callee(a,b,f,d,e):arguments.callee(a,b,c,f,e)},c=function(a,c,d){if(a.measureText(c).width<=d||c.length<1)return c;var e=b(a,c,0,c.length,d);return c.substring(0,e)+"\n"+arguments.callee(a,c.substring(e),d)},d="",e=a.split("\n"),f=0;f=2?parseInt(b[b.length-2],10):f.BitmapText.fonts[this.fontName].size,this.dirty=!0},f.BitmapText.prototype.updateText=function(){for(var a=f.BitmapText.fonts[this.fontName],b=new f.Point,c=null,d=[],e=0,g=[],h=0,i=this.fontSize/a.size,j=0;j=j;j++){var n=0;"right"==this.style.align?n=e-g[j]:"center"==this.style.align&&(n=(e-g[j])/2),m.push(n)}for(j=0;j0;)this.removeChild(this.getChildAt(0));this.updateText(),this.dirty=!1}f.DisplayObjectContainer.prototype.updateTransform.call(this)},f.BitmapText.fonts={},f.InteractionManager=function(a){this.stage=a,this.mouse=new f.InteractionData,this.touchs={},this.tempPoint=new f.Point,this.mouseoverEnabled=!0,this.pool=[],this.interactiveItems=[],this.last=0},f.InteractionManager.prototype.constructor=f.InteractionManager,f.InteractionManager.prototype.collectInteractiveSprite=function(a,b){for(var c=a.children,d=c.length,e=d-1;e>=0;e--){var f=c[e];f.interactive?(b.interactiveChildren=!0,this.interactiveItems.push(f),f.children.length>0&&this.collectInteractiveSprite(f,f)):(f.__iParent=null,f.children.length>0&&this.collectInteractiveSprite(f,b))}},f.InteractionManager.prototype.setTarget=function(a){window.navigator.msPointerEnabled&&(a.view.style["-ms-content-zooming"]="none",a.view.style["-ms-touch-action"]="none"),this.target=a,a.view.addEventListener("mousemove",this.onMouseMove.bind(this),!0),a.view.addEventListener("mousedown",this.onMouseDown.bind(this),!0),document.body.addEventListener("mouseup",this.onMouseUp.bind(this),!0),a.view.addEventListener("mouseout",this.onMouseOut.bind(this),!0),a.view.addEventListener("touchstart",this.onTouchStart.bind(this),!0),a.view.addEventListener("touchend",this.onTouchEnd.bind(this),!0),a.view.addEventListener("touchmove",this.onTouchMove.bind(this),!0)},f.InteractionManager.prototype.update=function(){if(this.target){var a=Date.now(),b=a-this.last;if(b=30*b/1e3,!(1>b)){if(this.last=a,this.dirty){this.dirty=!1;for(var c=this.interactiveItems.length,d=0;c>d;d++)this.interactiveItems[d].interactiveChildren=!1;this.interactiveItems=[],this.stage.interactive&&this.interactiveItems.push(this.stage),this.collectInteractiveSprite(this.stage,this.stage)}var e=this.interactiveItems.length;this.target.view.style.cursor="default";for(var d=0;e>d;d++){var f=this.interactiveItems[d];(f.mouseover||f.mouseout||f.buttonMode)&&(f.__hit=this.hitTest(f,this.mouse),this.mouse.target=f,f.__hit?(f.buttonMode&&(this.target.view.style.cursor="pointer"),f.__isOver||(f.mouseover&&f.mouseover(this.mouse),f.__isOver=!0)):f.__isOver&&(f.mouseout&&f.mouseout(this.mouse),f.__isOver=!1))}}}},f.InteractionManager.prototype.onMouseMove=function(a){this.mouse.originalEvent=a||window.event;var b=this.target.view.getBoundingClientRect();this.mouse.global.x=(a.clientX-b.left)*(this.target.width/b.width),this.mouse.global.y=(a.clientY-b.top)*(this.target.height/b.height);var c=this.interactiveItems.length;this.mouse.global;for(var d=0;c>d;d++){var e=this.interactiveItems[d];e.mousemove&&e.mousemove(this.mouse)}},f.InteractionManager.prototype.onMouseDown=function(a){this.mouse.originalEvent=a||window.event;var b=this.interactiveItems.length;this.mouse.global,this.stage;for(var c=0;b>c;c++){var d=this.interactiveItems[c];if((d.mousedown||d.click)&&(d.__mouseIsDown=!0,d.__hit=this.hitTest(d,this.mouse),d.__hit&&(d.mousedown&&d.mousedown(this.mouse),d.__isDown=!0,!d.interactiveChildren)))break}},f.InteractionManager.prototype.onMouseOut=function(){var a=this.interactiveItems.length;this.target.view.style.cursor="default";for(var b=0;a>b;b++){var c=this.interactiveItems[b];c.__isOver&&(this.mouse.target=c,c.mouseout&&c.mouseout(this.mouse),c.__isOver=!1)}},f.InteractionManager.prototype.onMouseUp=function(a){this.mouse.originalEvent=a||window.event,this.mouse.global;for(var b=this.interactiveItems.length,c=!1,d=0;b>d;d++){var e=this.interactiveItems[d];(e.mouseup||e.mouseupoutside||e.click)&&(e.__hit=this.hitTest(e,this.mouse),e.__hit&&!c?(e.mouseup&&e.mouseup(this.mouse),e.__isDown&&e.click&&e.click(this.mouse),e.interactiveChildren||(c=!0)):e.__isDown&&e.mouseupoutside&&e.mouseupoutside(this.mouse),e.__isDown=!1)}},f.InteractionManager.prototype.hitTest=function(a,b){var c=b.global;if(a.vcount!==f.visibleCount)return!1;var d=a instanceof f.Sprite,e=a.worldTransform,g=e[0],h=e[1],i=e[2],j=e[3],k=e[4],l=e[5],m=1/(g*k+h*-j),n=k*m*c.x+-h*m*c.y+(l*h-i*k)*m,o=g*m*c.y+-j*m*c.x+(-l*g+i*j)*m;if(b.target=a,a.hitArea&&a.hitArea.contains)return a.hitArea.contains(n,o)?(b.target=a,!0):!1;if(d){var p,q=a.texture.frame.width,r=a.texture.frame.height,s=-q*a.anchor.x;if(n>s&&s+q>n&&(p=-r*a.anchor.y,o>p&&p+r>o))return b.target=a,!0}for(var t=a.children.length,u=0;t>u;u++){var v=a.children[u],w=this.hitTest(v,b);if(w)return b.target=a,!0}return!1},f.InteractionManager.prototype.onTouchMove=function(a){for(var b=this.target.view.getBoundingClientRect(),c=a.changedTouches,d=0;dd;d++){var h=this.interactiveItems[d];h.touchmove&&h.touchmove(f)}},f.InteractionManager.prototype.onTouchStart=function(a){for(var b=this.target.view.getBoundingClientRect(),c=a.changedTouches,d=0;di;i++){var j=this.interactiveItems[i];if((j.touchstart||j.tap)&&(j.__hit=this.hitTest(j,g),j.__hit&&(j.touchstart&&j.touchstart(g),j.__isDown=!0,j.__touchData=g,!j.interactiveChildren)))break}}},f.InteractionManager.prototype.onTouchEnd=function(a){for(var b=this.target.view.getBoundingClientRect(),c=a.changedTouches,d=0;di;i++){var j=this.interactiveItems[i],k=j.__touchData;j.__hit=this.hitTest(j,f),k==f&&(f.originalEvent=a||window.event,(j.touchend||j.tap)&&(j.__hit&&!g?(j.touchend&&j.touchend(f),j.__isDown&&j.tap&&j.tap(f),j.interactiveChildren||(g=!0)):j.__isDown&&j.touchendoutside&&j.touchendoutside(f),j.__isDown=!1),j.__touchData=null)}this.pool.push(f),this.touchs[e.identifier]=null}},f.InteractionData=function(){this.global=new f.Point,this.local=new f.Point,this.target,this.originalEvent},f.InteractionData.prototype.getLocalPosition=function(a){var b=a.worldTransform,c=this.global,d=b[0],e=b[1],g=b[2],h=b[3],i=b[4],j=b[5],k=1/(d*i+e*-h);return new f.Point(i*k*c.x+-e*k*c.y+(j*e-g*i)*k,d*k*c.y+-h*k*c.x+(-j*d+g*h)*k)},f.InteractionData.prototype.constructor=f.InteractionData,f.Stage=function(a,b){f.DisplayObjectContainer.call(this),this.worldTransform=f.mat3.create(),this.interactive=b,this.interactionManager=new f.InteractionManager(this),this.dirty=!0,this.__childrenAdded=[],this.__childrenRemoved=[],this.stage=this,this.stage.hitArea=new f.Rectangle(0,0,1e5,1e5),this.setBackgroundColor(a),this.worldVisible=!0},f.Stage.prototype=Object.create(f.DisplayObjectContainer.prototype),f.Stage.prototype.constructor=f.Stage,f.Stage.prototype.updateTransform=function(){this.worldAlpha=1,this.vcount=f.visibleCount;for(var a=0,b=this.children.length;b>a;a++)this.children[a].updateTransform();this.dirty&&(this.dirty=!1,this.interactionManager.dirty=!0),this.interactive&&this.interactionManager.update()},f.Stage.prototype.setBackgroundColor=function(a){this.backgroundColor=a||0,this.backgroundColorSplit=d(this.backgroundColor);var b=this.backgroundColor.toString(16);b="000000".substr(0,6-b.length)+b,this.backgroundColorString="#"+b},f.Stage.prototype.getMousePosition=function(){return this.interactionManager.mouse.global};for(var h=0,i=["ms","moz","webkit","o"],j=0;j>>>>>>>>"),console.log("_");var b=0,c=a.first;for(console.log(c);c._iNext;)if(b++,c=c._iNext,console.log(c),b>100){console.log("BREAK");break}},f.EventTarget=function(){var a={};this.addEventListener=this.on=function(b,c){void 0===a[b]&&(a[b]=[]),-1===a[b].indexOf(c)&&a[b].push(c)},this.dispatchEvent=this.emit=function(b){for(var c in a[b.type])a[b.type][c](b)},this.removeEventListener=this.off=function(b,c){var d=a[b].indexOf(c);-1!==d&&a[b].splice(d,1)}},f.autoDetectRenderer=function(a,b,c,d,e){a||(a=800),b||(b=600);var g=function(){try{return!!window.WebGLRenderingContext&&!!document.createElement("canvas").getContext("experimental-webgl")}catch(a){return!1}}();if(g){var h=-1!=navigator.userAgent.toLowerCase().indexOf("msie");g=!h}return g?new f.WebGLRenderer(a,b,c,d,e):new f.CanvasRenderer(a,b,c,d)},f.PolyK={},f.PolyK.Triangulate=function(a){var b=!0,c=a.length>>1;if(3>c)return[];for(var d=[],e=[],g=0;c>g;g++)e.push(g);for(var g=0,h=c;h>3;){var i=e[(g+0)%h],j=e[(g+1)%h],k=e[(g+2)%h],l=a[2*i],m=a[2*i+1],n=a[2*j],o=a[2*j+1],p=a[2*k],q=a[2*k+1],r=!1;if(f.PolyK._convex(l,m,n,o,p,q,b)){r=!0;for(var s=0;h>s;s++){var t=e[s];if(t!=i&&t!=j&&t!=k&&f.PolyK._PointInTriangle(a[2*t],a[2*t+1],l,m,n,o,p,q)){r=!1;break}}}if(r)d.push(i,j,k),e.splice((g+1)%h,1),h--,g=0;else if(g++>3*h){if(!b)return console.log("PIXI Warning: shape too complex to fill"),[];var d=[];e=[];for(var g=0;c>g;g++)e.push(g);g=0,h=c,b=!1}}return d.push(e[0],e[1],e[2]),d},f.PolyK._PointInTriangle=function(a,b,c,d,e,f,g,h){var i=g-c,j=h-d,k=e-c,l=f-d,m=a-c,n=b-d,o=i*i+j*j,p=i*k+j*l,q=i*m+j*n,r=k*k+l*l,s=k*m+l*n,t=1/(o*r-p*p),u=(r*q-p*s)*t,v=(o*s-p*q)*t;return u>=0&&v>=0&&1>u+v},f.PolyK._convex=function(a,b,c,d,e,f,g){return(b-d)*(e-c)+(c-a)*(f-d)>=0==g},f.shaderFragmentSrc=["precision mediump float;","varying vec2 vTextureCoord;","varying float vColor;","uniform sampler2D uSampler;","void main(void) {","gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.x, vTextureCoord.y));","gl_FragColor = gl_FragColor * vColor;","}"],f.shaderVertexSrc=["attribute vec2 aVertexPosition;","attribute vec2 aTextureCoord;","attribute float aColor;","uniform vec2 projectionVector;","varying vec2 vTextureCoord;","varying float vColor;","void main(void) {","gl_Position = vec4( aVertexPosition.x / projectionVector.x -1.0, aVertexPosition.y / -projectionVector.y + 1.0 , 0.0, 1.0);","vTextureCoord = aTextureCoord;","vColor = aColor;","}"],f.stripShaderFragmentSrc=["precision mediump float;","varying vec2 vTextureCoord;","varying float vColor;","uniform float alpha;","uniform sampler2D uSampler;","void main(void) {","gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.x, vTextureCoord.y));","gl_FragColor = gl_FragColor * alpha;","}"],f.stripShaderVertexSrc=["attribute vec2 aVertexPosition;","attribute vec2 aTextureCoord;","attribute float aColor;","uniform mat3 translationMatrix;","uniform vec2 projectionVector;","varying vec2 vTextureCoord;","varying float vColor;","void main(void) {","vec3 v = translationMatrix * vec3(aVertexPosition, 1.0);","gl_Position = vec4( v.x / projectionVector.x -1.0, v.y / -projectionVector.y + 1.0 , 0.0, 1.0);","vTextureCoord = aTextureCoord;","vColor = aColor;","}"],f.primitiveShaderFragmentSrc=["precision mediump float;","varying vec4 vColor;","void main(void) {","gl_FragColor = vColor;","}"],f.primitiveShaderVertexSrc=["attribute vec2 aVertexPosition;","attribute vec4 aColor;","uniform mat3 translationMatrix;","uniform vec2 projectionVector;","uniform float alpha;","varying vec4 vColor;","void main(void) {","vec3 v = translationMatrix * vec3(aVertexPosition, 1.0);","gl_Position = vec4( v.x / projectionVector.x -1.0, v.y / -projectionVector.y + 1.0 , 0.0, 1.0);","vColor = aColor * alpha;","}"],f.initPrimitiveShader=function(){var a=f.gl,b=f.compileProgram(f.primitiveShaderVertexSrc,f.primitiveShaderFragmentSrc);a.useProgram(b),b.vertexPositionAttribute=a.getAttribLocation(b,"aVertexPosition"),b.colorAttribute=a.getAttribLocation(b,"aColor"),b.projectionVector=a.getUniformLocation(b,"projectionVector"),b.translationMatrix=a.getUniformLocation(b,"translationMatrix"),b.alpha=a.getUniformLocation(b,"alpha"),f.primitiveProgram=b},f.initDefaultShader=function(){var a=this.gl,b=f.compileProgram(f.shaderVertexSrc,f.shaderFragmentSrc);a.useProgram(b),b.vertexPositionAttribute=a.getAttribLocation(b,"aVertexPosition"),b.projectionVector=a.getUniformLocation(b,"projectionVector"),b.textureCoordAttribute=a.getAttribLocation(b,"aTextureCoord"),b.colorAttribute=a.getAttribLocation(b,"aColor"),b.samplerUniform=a.getUniformLocation(b,"uSampler"),f.shaderProgram=b},f.initDefaultStripShader=function(){var a=this.gl,b=f.compileProgram(f.stripShaderVertexSrc,f.stripShaderFragmentSrc);a.useProgram(b),b.vertexPositionAttribute=a.getAttribLocation(b,"aVertexPosition"),b.projectionVector=a.getUniformLocation(b,"projectionVector"),b.textureCoordAttribute=a.getAttribLocation(b,"aTextureCoord"),b.translationMatrix=a.getUniformLocation(b,"translationMatrix"),b.alpha=a.getUniformLocation(b,"alpha"),b.colorAttribute=a.getAttribLocation(b,"aColor"),b.projectionVector=a.getUniformLocation(b,"projectionVector"),b.samplerUniform=a.getUniformLocation(b,"uSampler"),f.stripShaderProgram=b},f.CompileVertexShader=function(a,b){return f._CompileShader(a,b,a.VERTEX_SHADER)},f.CompileFragmentShader=function(a,b){return f._CompileShader(a,b,a.FRAGMENT_SHADER)},f._CompileShader=function(a,b,c){var d=b.join("\n"),e=a.createShader(c);return a.shaderSource(e,d),a.compileShader(e),a.getShaderParameter(e,a.COMPILE_STATUS)?e:(alert(a.getShaderInfoLog(e)),null)},f.compileProgram=function(a,b){var c=f.gl,d=f.CompileFragmentShader(c,b),e=f.CompileVertexShader(c,a),g=c.createProgram();return c.attachShader(g,e),c.attachShader(g,d),c.linkProgram(g),c.getProgramParameter(g,c.LINK_STATUS)||alert("Could not initialise shaders"),g},f.activateDefaultShader=function(){var a=f.gl,b=f.shaderProgram; +a.useProgram(b),a.enableVertexAttribArray(b.vertexPositionAttribute),a.enableVertexAttribArray(b.textureCoordAttribute),a.enableVertexAttribArray(b.colorAttribute)},f.activatePrimitiveShader=function(){var a=f.gl;a.disableVertexAttribArray(f.shaderProgram.textureCoordAttribute),a.disableVertexAttribArray(f.shaderProgram.colorAttribute),a.useProgram(f.primitiveProgram),a.enableVertexAttribArray(f.primitiveProgram.vertexPositionAttribute),a.enableVertexAttribArray(f.primitiveProgram.colorAttribute)},f.WebGLGraphics=function(){},f.WebGLGraphics.renderGraphics=function(a,b){var c=f.gl;a._webGL||(a._webGL={points:[],indices:[],lastIndex:0,buffer:c.createBuffer(),indexBuffer:c.createBuffer()}),a.dirty&&(a.dirty=!1,a.clearDirty&&(a.clearDirty=!1,a._webGL.lastIndex=0,a._webGL.points=[],a._webGL.indices=[]),f.WebGLGraphics.updateGraphics(a)),f.activatePrimitiveShader();var d=f.mat3.clone(a.worldTransform);f.mat3.transpose(d),c.blendFunc(c.ONE,c.ONE_MINUS_SRC_ALPHA),c.uniformMatrix3fv(f.primitiveProgram.translationMatrix,!1,d),c.uniform2f(f.primitiveProgram.projectionVector,b.x,b.y),c.uniform1f(f.primitiveProgram.alpha,a.worldAlpha),c.bindBuffer(c.ARRAY_BUFFER,a._webGL.buffer),c.vertexAttribPointer(f.shaderProgram.vertexPositionAttribute,2,c.FLOAT,!1,0,0),c.vertexAttribPointer(f.primitiveProgram.vertexPositionAttribute,2,c.FLOAT,!1,24,0),c.vertexAttribPointer(f.primitiveProgram.colorAttribute,4,c.FLOAT,!1,24,8),c.bindBuffer(c.ELEMENT_ARRAY_BUFFER,a._webGL.indexBuffer),c.drawElements(c.TRIANGLE_STRIP,a._webGL.indices.length,c.UNSIGNED_SHORT,0),f.activateDefaultShader()},f.WebGLGraphics.updateGraphics=function(a){for(var b=a._webGL.lastIndex;b3&&f.WebGLGraphics.buildPoly(c,a._webGL),c.lineWidth>0&&f.WebGLGraphics.buildLine(c,a._webGL)):c.type==f.Graphics.RECT?f.WebGLGraphics.buildRectangle(c,a._webGL):(c.type==f.Graphics.CIRC||c.type==f.Graphics.ELIP)&&f.WebGLGraphics.buildCircle(c,a._webGL)}a._webGL.lastIndex=a.graphicsData.length;var d=f.gl;a._webGL.glPoints=new Float32Array(a._webGL.points),d.bindBuffer(d.ARRAY_BUFFER,a._webGL.buffer),d.bufferData(d.ARRAY_BUFFER,a._webGL.glPoints,d.STATIC_DRAW),a._webGL.glIndicies=new Uint16Array(a._webGL.indices),d.bindBuffer(d.ELEMENT_ARRAY_BUFFER,a._webGL.indexBuffer),d.bufferData(d.ELEMENT_ARRAY_BUFFER,a._webGL.glIndicies,d.STATIC_DRAW)},f.WebGLGraphics.buildRectangle=function(a,b){var c=a.points,e=c[0],g=c[1],h=c[2],i=c[3];if(a.fill){var j=d(a.fillColor),k=a.fillAlpha,l=j[0]*k,m=j[1]*k,n=j[2]*k,o=b.points,p=b.indices,q=o.length/6;o.push(e,g),o.push(l,m,n,k),o.push(e+h,g),o.push(l,m,n,k),o.push(e,g+i),o.push(l,m,n,k),o.push(e+h,g+i),o.push(l,m,n,k),p.push(q,q,q+1,q+2,q+3,q+3)}a.lineWidth&&(a.points=[e,g,e+h,g,e+h,g+i,e,g+i,e,g],f.WebGLGraphics.buildLine(a,b))},f.WebGLGraphics.buildCircle=function(a,b){var c=a.points,e=c[0],g=c[1],h=c[2],i=c[3],j=40,k=2*Math.PI/j;if(a.fill){var l=d(a.fillColor),m=a.fillAlpha,n=l[0]*m,o=l[1]*m,p=l[2]*m,q=b.points,r=b.indices,s=q.length/6;r.push(s);for(var t=0;j+1>t;t++)q.push(e,g,n,o,p,m),q.push(e+Math.sin(k*t)*h,g+Math.cos(k*t)*i,n,o,p,m),r.push(s++,s++);r.push(s-1)}if(a.lineWidth){a.points=[];for(var t=0;j+1>t;t++)a.points.push(e+Math.sin(k*t)*h,g+Math.cos(k*t)*i);f.WebGLGraphics.buildLine(a,b)}},f.WebGLGraphics.buildLine=function(a,b){var c=a.points;if(0!=c.length){var e=new f.Point(c[0],c[1]),g=new f.Point(c[c.length-2],c[c.length-1]);if(e.x==g.x&&e.y==g.y){c.pop(),c.pop(),g=new f.Point(c[c.length-2],c[c.length-1]);var h=g.x+.5*(e.x-g.x),i=g.y+.5*(e.y-g.y);c.unshift(h,i),c.push(h,i)}var j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E=b.points,F=b.indices,G=c.length/2,H=c.length,I=E.length/6,J=a.lineWidth/2,K=d(a.lineColor),L=a.lineAlpha,M=K[0]*L,N=K[1]*L,O=K[2]*L;j=c[0],k=c[1],l=c[2],m=c[3],p=-(k-m),q=j-l,D=Math.sqrt(p*p+q*q),p/=D,q/=D,p*=J,q*=J,E.push(j-p,k-q,M,N,O,L),E.push(j+p,k+q,M,N,O,L);for(var P=1;G-1>P;P++)j=c[2*(P-1)],k=c[2*(P-1)+1],l=c[2*P],m=c[2*P+1],n=c[2*(P+1)],o=c[2*(P+1)+1],p=-(k-m),q=j-l,D=Math.sqrt(p*p+q*q),p/=D,q/=D,p*=J,q*=J,r=-(m-o),s=l-n,D=Math.sqrt(r*r+s*s),r/=D,s/=D,r*=J,s*=J,v=-q+k-(-q+m),w=-p+l-(-p+j),x=(-p+j)*(-q+m)-(-p+l)*(-q+k),y=-s+o-(-s+m),z=-r+l-(-r+n),A=(-r+n)*(-s+m)-(-r+l)*(-s+o),B=v*z-y*w,0==B&&(B+=1),px=(w*A-z*x)/B,py=(y*x-v*A)/B,C=(px-l)*(px-l)+(py-m)+(py-m),C>19600?(t=p-r,u=q-s,D=Math.sqrt(t*t+u*u),t/=D,u/=D,t*=J,u*=J,E.push(l-t,m-u),E.push(M,N,O,L),E.push(l+t,m+u),E.push(M,N,O,L),E.push(l-t,m-u),E.push(M,N,O,L),H++):(E.push(px,py),E.push(M,N,O,L),E.push(l-(px-l),m-(py-m)),E.push(M,N,O,L));j=c[2*(G-2)],k=c[2*(G-2)+1],l=c[2*(G-1)],m=c[2*(G-1)+1],p=-(k-m),q=j-l,D=Math.sqrt(p*p+q*q),p/=D,q/=D,p*=J,q*=J,E.push(l-p,m-q),E.push(M,N,O,L),E.push(l+p,m+q),E.push(M,N,O,L),F.push(I);for(var P=0;H>P;P++)F.push(I++);F.push(I-1)}},f.WebGLGraphics.buildPoly=function(a,b){var c=a.points;if(!(c.length<6)){for(var e=b.points,g=b.indices,h=c.length/2,i=d(a.fillColor),j=a.fillAlpha,k=i[0]*j,l=i[1]*j,m=i[2]*j,n=f.PolyK.Triangulate(c),o=e.length/6,p=0;pp;p++)e.push(c[2*p],c[2*p+1],k,l,m,j)}},f._defaultFrame=new f.Rectangle(0,0,1,1),f.gl,f.WebGLRenderer=function(a,b,c,d,e){this.transparent=!!d,this.width=a||800,this.height=b||600,this.view=c||document.createElement("canvas"),this.view.width=this.width,this.view.height=this.height;var g=this;this.view.addEventListener("webglcontextlost",function(a){g.handleContextLost(a)},!1),this.view.addEventListener("webglcontextrestored",function(a){g.handleContextRestored(a)},!1),this.batchs=[];try{f.gl=this.gl=this.view.getContext("experimental-webgl",{alpha:this.transparent,antialias:!!e,premultipliedAlpha:!1,stencil:!0})}catch(h){throw new Error(" This browser does not support webGL. Try using the canvas renderer"+this)}f.initPrimitiveShader(),f.initDefaultShader(),f.initDefaultStripShader(),f.activateDefaultShader();var i=this.gl;f.WebGLRenderer.gl=i,this.batch=new f.WebGLBatch(i),i.disable(i.DEPTH_TEST),i.disable(i.CULL_FACE),i.enable(i.BLEND),i.colorMask(!0,!0,!0,this.transparent),f.projection=new f.Point(400,300),this.resize(this.width,this.height),this.contextLost=!1,this.stageRenderGroup=new f.WebGLRenderGroup(this.gl)},f.WebGLRenderer.prototype.constructor=f.WebGLRenderer,f.WebGLRenderer.getBatch=function(){return 0==f._batchs.length?new f.WebGLBatch(f.WebGLRenderer.gl):f._batchs.pop()},f.WebGLRenderer.returnBatch=function(a){a.clean(),f._batchs.push(a)},f.WebGLRenderer.prototype.render=function(a){if(!this.contextLost){this.__stage!==a&&(this.__stage=a,this.stageRenderGroup.setRenderable(a)),f.WebGLRenderer.updateTextures(),f.visibleCount++,a.updateTransform();var b=this.gl;if(b.colorMask(!0,!0,!0,this.transparent),b.viewport(0,0,this.width,this.height),b.bindFramebuffer(b.FRAMEBUFFER,null),b.clearColor(a.backgroundColorSplit[0],a.backgroundColorSplit[1],a.backgroundColorSplit[2],!this.transparent),b.clear(b.COLOR_BUFFER_BIT),this.stageRenderGroup.backgroundColor=a.backgroundColorSplit,this.stageRenderGroup.render(f.projection),a.interactive&&(a._interactiveEventsAdded||(a._interactiveEventsAdded=!0,a.interactionManager.setTarget(this))),f.Texture.frameUpdates.length>0){for(var c=0;cc;c++){var d=6*c,e=4*c;this.indices[d+0]=e+0,this.indices[d+1]=e+1,this.indices[d+2]=e+2,this.indices[d+3]=e+0,this.indices[d+4]=e+2,this.indices[d+5]=e+3}a.bindBuffer(a.ELEMENT_ARRAY_BUFFER,this.indexBuffer),a.bufferData(a.ELEMENT_ARRAY_BUFFER,this.indices,a.STATIC_DRAW)},f.WebGLBatch.prototype.refresh=function(){this.gl,this.dynamicSize0;)n=n.children[n.children.length-1],n.renderable&&(m=n);if(m instanceof f.Sprite){l=m.batch;var k=l.head;if(k==m)g=0;else for(g=1;k.__next!=m;)g++,k=k.__next}else l=m;if(j==l)return j instanceof f.WebGLBatch?j.render(d,g+1):this.renderSpecial(j,b),void 0;e=this.batchs.indexOf(j),h=this.batchs.indexOf(l),j instanceof f.WebGLBatch?j.render(d):this.renderSpecial(j,b);for(var o=e+1;h>o;o++)renderable=this.batchs[o],renderable instanceof f.WebGLBatch?this.batchs[o].render():this.renderSpecial(renderable,b);l instanceof f.WebGLBatch?l.render(0,g+1):this.renderSpecial(l,b)},f.WebGLRenderGroup.prototype.renderSpecial=function(a,b){var c=a.vcount===f.visibleCount;if(a instanceof f.TilingSprite)c&&this.renderTilingSprite(a,b);else if(a instanceof f.Strip)c&&this.renderStrip(a,b);else if(a instanceof f.CustomRenderable)c&&a.renderWebGL(this,b);else if(a instanceof f.Graphics)c&&a.renderable&&f.WebGLGraphics.renderGraphics(a,b);else if(a instanceof f.FilterBlock){var d=f.gl;a.open?(d.enable(d.STENCIL_TEST),d.colorMask(!1,!1,!1,!1),d.stencilFunc(d.ALWAYS,1,255),d.stencilOp(d.KEEP,d.KEEP,d.REPLACE),f.WebGLGraphics.renderGraphics(a.mask,b),d.colorMask(!0,!0,!0,!0),d.stencilFunc(d.NOTEQUAL,0,255),d.stencilOp(d.KEEP,d.KEEP,d.KEEP)):d.disable(d.STENCIL_TEST)}},f.WebGLRenderGroup.prototype.updateTexture=function(a){this.removeObject(a);for(var b=a.first;b!=this.root&&(b=b._iPrev,!b.renderable||!b.__renderGroup););for(var c=a.last;c._iNext&&(c=c._iNext,!c.renderable||!c.__renderGroup););this.insertObject(a,b,c)},f.WebGLRenderGroup.prototype.addFilterBlocks=function(a,b){a.__renderGroup=this,b.__renderGroup=this;for(var c=a;c!=this.root&&(c=c._iPrev,!c.renderable||!c.__renderGroup););this.insertAfter(a,c);for(var d=b;d!=this.root&&(d=d._iPrev,!d.renderable||!d.__renderGroup););this.insertAfter(b,d)},f.WebGLRenderGroup.prototype.removeFilterBlocks=function(a,b){this.removeObject(a),this.removeObject(b)},f.WebGLRenderGroup.prototype.addDisplayObjectAndChildren=function(a){a.__renderGroup&&a.__renderGroup.removeDisplayObjectAndChildren(a);for(var b=a.first;b!=this.root.first&&(b=b._iPrev,!b.renderable||!b.__renderGroup););for(var c=a.last;c._iNext&&(c=c._iNext,!c.renderable||!c.__renderGroup););var d=a.first,e=a.last._iNext;do d.__renderGroup=this,d.renderable&&(this.insertObject(d,b,c),b=d),d=d._iNext;while(d!=e)},f.WebGLRenderGroup.prototype.removeDisplayObjectAndChildren=function(a){if(a.__renderGroup==this){a.last;do a.__renderGroup=null,a.renderable&&this.removeObject(a),a=a._iNext;while(a)}},f.WebGLRenderGroup.prototype.insertObject=function(a,b,c){var d=b,e=c;if(a instanceof f.Sprite){var g,h;if(d instanceof f.Sprite){if(g=d.batch,g&&g.texture==a.texture.baseTexture&&g.blendMode==a.blendMode)return g.insertAfter(a,d),void 0}else g=d;if(e)if(e instanceof f.Sprite){if(h=e.batch){if(h.texture==a.texture.baseTexture&&h.blendMode==a.blendMode)return h.insertBefore(a,e),void 0;if(h==g){var i=g.split(e),j=f.WebGLRenderer.getBatch(),k=this.batchs.indexOf(g);return j.init(a),this.batchs.splice(k+1,0,j,i),void 0}}}else h=e;var j=f.WebGLRenderer.getBatch();if(j.init(a),g){var k=this.batchs.indexOf(g);this.batchs.splice(k+1,0,j)}else this.batchs.push(j)}else a instanceof f.TilingSprite?this.initTilingSprite(a):a instanceof f.Strip&&this.initStrip(a),this.insertAfter(a,d)},f.WebGLRenderGroup.prototype.insertAfter=function(a,b){if(b instanceof f.Sprite){var c=b.batch;if(c)if(c.tail==b){var d=this.batchs.indexOf(c);this.batchs.splice(d+1,0,a)}else{var e=c.split(b.__next),d=this.batchs.indexOf(c);this.batchs.splice(d+1,0,a,e)}else this.batchs.push(a)}else{var d=this.batchs.indexOf(b);this.batchs.splice(d+1,0,a)}},f.WebGLRenderGroup.prototype.removeObject=function(a){var b;if(a instanceof f.Sprite){var c=a.batch;if(!c)return;c.remove(a),0==c.size&&(b=c)}else b=a;if(b){var d=this.batchs.indexOf(b);if(-1==d)return;if(0==d||d==this.batchs.length-1)return this.batchs.splice(d,1),b instanceof f.WebGLBatch&&f.WebGLRenderer.returnBatch(b),void 0;if(this.batchs[d-1]instanceof f.WebGLBatch&&this.batchs[d+1]instanceof f.WebGLBatch&&this.batchs[d-1].texture==this.batchs[d+1].texture&&this.batchs[d-1].blendMode==this.batchs[d+1].blendMode)return this.batchs[d-1].merge(this.batchs[d+1]),b instanceof f.WebGLBatch&&f.WebGLRenderer.returnBatch(b),f.WebGLRenderer.returnBatch(this.batchs[d+1]),this.batchs.splice(d,2),void 0;this.batchs.splice(d,1),b instanceof f.WebGLBatch&&f.WebGLRenderer.returnBatch(b)}},f.WebGLRenderGroup.prototype.initTilingSprite=function(a){var b=this.gl;a.verticies=new Float32Array([0,0,a.width,0,a.width,a.height,0,a.height]),a.uvs=new Float32Array([0,0,1,0,1,1,0,1]),a.colors=new Float32Array([1,1,1,1]),a.indices=new Uint16Array([0,1,3,2]),a._vertexBuffer=b.createBuffer(),a._indexBuffer=b.createBuffer(),a._uvBuffer=b.createBuffer(),a._colorBuffer=b.createBuffer(),b.bindBuffer(b.ARRAY_BUFFER,a._vertexBuffer),b.bufferData(b.ARRAY_BUFFER,a.verticies,b.STATIC_DRAW),b.bindBuffer(b.ARRAY_BUFFER,a._uvBuffer),b.bufferData(b.ARRAY_BUFFER,a.uvs,b.DYNAMIC_DRAW),b.bindBuffer(b.ARRAY_BUFFER,a._colorBuffer),b.bufferData(b.ARRAY_BUFFER,a.colors,b.STATIC_DRAW),b.bindBuffer(b.ELEMENT_ARRAY_BUFFER,a._indexBuffer),b.bufferData(b.ELEMENT_ARRAY_BUFFER,a.indices,b.STATIC_DRAW),a.texture.baseTexture._glTexture?(b.bindTexture(b.TEXTURE_2D,a.texture.baseTexture._glTexture),b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_S,b.REPEAT),b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_T,b.REPEAT),a.texture.baseTexture._powerOf2=!0):a.texture.baseTexture._powerOf2=!0},f.WebGLRenderGroup.prototype.renderStrip=function(a,b){var c=this.gl,d=f.shaderProgram;c.useProgram(f.stripShaderProgram);var e=f.mat3.clone(a.worldTransform);f.mat3.transpose(e),c.uniformMatrix3fv(f.stripShaderProgram.translationMatrix,!1,e),c.uniform2f(f.stripShaderProgram.projectionVector,b.x,b.y),c.uniform1f(f.stripShaderProgram.alpha,a.worldAlpha),a.dirty?(a.dirty=!1,c.bindBuffer(c.ARRAY_BUFFER,a._vertexBuffer),c.bufferData(c.ARRAY_BUFFER,a.verticies,c.STATIC_DRAW),c.vertexAttribPointer(d.vertexPositionAttribute,2,c.FLOAT,!1,0,0),c.bindBuffer(c.ARRAY_BUFFER,a._uvBuffer),c.bufferData(c.ARRAY_BUFFER,a.uvs,c.STATIC_DRAW),c.vertexAttribPointer(d.textureCoordAttribute,2,c.FLOAT,!1,0,0),c.activeTexture(c.TEXTURE0),c.bindTexture(c.TEXTURE_2D,a.texture.baseTexture._glTexture),c.bindBuffer(c.ARRAY_BUFFER,a._colorBuffer),c.bufferData(c.ARRAY_BUFFER,a.colors,c.STATIC_DRAW),c.vertexAttribPointer(d.colorAttribute,1,c.FLOAT,!1,0,0),c.bindBuffer(c.ELEMENT_ARRAY_BUFFER,a._indexBuffer),c.bufferData(c.ELEMENT_ARRAY_BUFFER,a.indices,c.STATIC_DRAW)):(c.bindBuffer(c.ARRAY_BUFFER,a._vertexBuffer),c.bufferSubData(c.ARRAY_BUFFER,0,a.verticies),c.vertexAttribPointer(d.vertexPositionAttribute,2,c.FLOAT,!1,0,0),c.bindBuffer(c.ARRAY_BUFFER,a._uvBuffer),c.vertexAttribPointer(d.textureCoordAttribute,2,c.FLOAT,!1,0,0),c.activeTexture(c.TEXTURE0),c.bindTexture(c.TEXTURE_2D,a.texture.baseTexture._glTexture),c.bindBuffer(c.ARRAY_BUFFER,a._colorBuffer),c.vertexAttribPointer(d.colorAttribute,1,c.FLOAT,!1,0,0),c.bindBuffer(c.ELEMENT_ARRAY_BUFFER,a._indexBuffer)),c.drawElements(c.TRIANGLE_STRIP,a.indices.length,c.UNSIGNED_SHORT,0),c.useProgram(f.shaderProgram)},f.WebGLRenderGroup.prototype.renderTilingSprite=function(a,b){var c=this.gl;f.shaderProgram;var d=a.tilePosition,e=a.tileScale,g=d.x/a.texture.baseTexture.width,h=d.y/a.texture.baseTexture.height,i=a.width/a.texture.baseTexture.width/e.x,j=a.height/a.texture.baseTexture.height/e.y;a.uvs[0]=0-g,a.uvs[1]=0-h,a.uvs[2]=1*i-g,a.uvs[3]=0-h,a.uvs[4]=1*i-g,a.uvs[5]=1*j-h,a.uvs[6]=0-g,a.uvs[7]=1*j-h,c.bindBuffer(c.ARRAY_BUFFER,a._uvBuffer),c.bufferSubData(c.ARRAY_BUFFER,0,a.uvs),this.renderStrip(a,b)},f.WebGLRenderGroup.prototype.initStrip=function(a){var b=this.gl;this.shaderProgram,a._vertexBuffer=b.createBuffer(),a._indexBuffer=b.createBuffer(),a._uvBuffer=b.createBuffer(),a._colorBuffer=b.createBuffer(),b.bindBuffer(b.ARRAY_BUFFER,a._vertexBuffer),b.bufferData(b.ARRAY_BUFFER,a.verticies,b.DYNAMIC_DRAW),b.bindBuffer(b.ARRAY_BUFFER,a._uvBuffer),b.bufferData(b.ARRAY_BUFFER,a.uvs,b.STATIC_DRAW),b.bindBuffer(b.ARRAY_BUFFER,a._colorBuffer),b.bufferData(b.ARRAY_BUFFER,a.colors,b.STATIC_DRAW),b.bindBuffer(b.ELEMENT_ARRAY_BUFFER,a._indexBuffer),b.bufferData(b.ELEMENT_ARRAY_BUFFER,a.indices,b.STATIC_DRAW)},f.CanvasRenderer=function(a,b,c,d){this.transparent=d,this.width=a||800,this.height=b||600,this.view=c||document.createElement("canvas"),this.context=this.view.getContext("2d"),this.refresh=!0,this.view.width=this.width,this.view.height=this.height,this.count=0},f.CanvasRenderer.prototype.constructor=f.CanvasRenderer,f.CanvasRenderer.prototype.render=function(a){f.texturesToUpdate=[],f.texturesToDestroy=[],f.visibleCount++,a.updateTransform(),this.view.style.backgroundColor==a.backgroundColorString||this.transparent||(this.view.style.backgroundColor=a.backgroundColorString),this.context.setTransform(1,0,0,1,0,0),this.context.clearRect(0,0,this.width,this.height),this.renderDisplayObject(a),a.interactive&&(a._interactiveEventsAdded||(a._interactiveEventsAdded=!0,a.interactionManager.setTarget(this))),f.Texture.frameUpdates.length>0&&(f.Texture.frameUpdates=[])},f.CanvasRenderer.prototype.resize=function(a,b){this.width=a,this.height=b,this.view.width=a,this.view.height=b},f.CanvasRenderer.prototype.renderDisplayObject=function(a){var b,c=this.context;c.globalCompositeOperation="source-over";var d=a.last._iNext;a=a.first;do if(b=a.worldTransform,a.visible)if(a.renderable){if(a instanceof f.Sprite){var e=a.texture.frame;e&&(c.globalAlpha=a.worldAlpha,c.setTransform(b[0],b[3],b[1],b[4],b[2],b[5]),c.drawImage(a.texture.baseTexture.source,e.x,e.y,e.width,e.height,a.anchor.x*-e.width,a.anchor.y*-e.height,e.width,e.height))}else if(a instanceof f.Strip)c.setTransform(b[0],b[3],b[1],b[4],b[2],b[5]),this.renderStrip(a);else if(a instanceof f.TilingSprite)c.setTransform(b[0],b[3],b[1],b[4],b[2],b[5]),this.renderTilingSprite(a);else if(a instanceof f.CustomRenderable)a.renderCanvas(this);else if(a instanceof f.Graphics)c.setTransform(b[0],b[3],b[1],b[4],b[2],b[5]),f.CanvasGraphics.renderGraphics(a,c);else if(a instanceof f.FilterBlock)if(a.open){c.save();var g=a.mask.alpha,h=a.mask.worldTransform;c.setTransform(h[0],h[3],h[1],h[4],h[2],h[5]),a.mask.worldAlpha=.5,c.worldAlpha=0,f.CanvasGraphics.renderGraphicsMask(a.mask,c),c.clip(),a.mask.worldAlpha=g}else c.restore();a=a._iNext}else a=a._iNext;else a=a.last._iNext;while(a!=d)},f.CanvasRenderer.prototype.renderStripFlat=function(a){var b=this.context,c=a.verticies;a.uvs;var d=c.length/2;this.count++,b.beginPath();for(var e=1;d-2>e;e++){var f=2*e,g=c[f],h=c[f+2],i=c[f+4],j=c[f+1],k=c[f+3],l=c[f+5];b.moveTo(g,j),b.lineTo(h,k),b.lineTo(i,l)}b.fillStyle="#FF0000",b.fill(),b.closePath()},f.CanvasRenderer.prototype.renderTilingSprite=function(a){var b=this.context;b.globalAlpha=a.worldAlpha,a.__tilePattern||(a.__tilePattern=b.createPattern(a.texture.baseTexture.source,"repeat")),b.beginPath();var c=a.tilePosition,d=a.tileScale;b.scale(d.x,d.y),b.translate(c.x,c.y),b.fillStyle=a.__tilePattern,b.fillRect(-c.x,-c.y,a.width/d.x,a.height/d.y),b.scale(1/d.x,1/d.y),b.translate(-c.x,-c.y),b.closePath()},f.CanvasRenderer.prototype.renderStrip=function(a){var b=this.context,c=a.verticies,d=a.uvs,e=c.length/2;this.count++;for(var f=1;e-2>f;f++){var g=2*f,h=c[g],i=c[g+2],j=c[g+4],k=c[g+1],l=c[g+3],m=c[g+5],n=d[g]*a.texture.width,o=d[g+2]*a.texture.width,p=d[g+4]*a.texture.width,q=d[g+1]*a.texture.height,r=d[g+3]*a.texture.height,s=d[g+5]*a.texture.height;b.save(),b.beginPath(),b.moveTo(h,k),b.lineTo(i,l),b.lineTo(j,m),b.closePath(),b.clip();var t=n*r+q*p+o*s-r*p-q*o-n*s,u=h*r+q*j+i*s-r*j-q*i-h*s,v=n*i+h*p+o*j-i*p-h*o-n*j,w=n*r*j+q*i*p+h*o*s-h*r*p-q*o*j-n*i*s,x=k*r+q*m+l*s-r*m-q*l-k*s,y=n*l+k*p+o*m-l*p-k*o-n*m,z=n*r*m+q*l*p+k*o*s-k*r*p-q*o*m-n*l*s;b.transform(u/t,x/t,v/t,y/t,w/t,z/t),b.drawImage(a.texture.baseTexture.source,0,0),b.restore()}},f.CanvasGraphics=function(){},f.CanvasGraphics.renderGraphics=function(a,b){for(var c=a.worldAlpha,d=0;d1&&(c=1,console.log("Pixi.js warning: masks in canvas can only mask using the first path in the graphics object"));for(var d=0;1>d;d++){var e=a.graphicsData[d],g=e.points;if(e.type==f.Graphics.POLY){b.beginPath(),b.moveTo(g[0],g[1]);for(var h=1;hh;h++){var f=a[h],i=4*h,j=h/(g-1);h%2?(b[i]=j,b[i+1]=0,b[i+2]=j,b[i+3]=1):(b[i]=j,b[i+1]=0,b[i+2]=j,b[i+3]=1),i=2*h,d[i]=1,d[i+1]=1,i=2*h,c[i]=i,c[i+1]=i+1,e=f}}},f.Rope.prototype.updateTransform=function(){var a=this.points;if(!(a.length<1)){var b,c=this.verticies,d=a[0],e={x:0,y:0},g=a[0];this.count-=.2,c[0]=g.x+e.x,c[1]=g.y+e.y,c[2]=g.x-e.x,c[3]=g.y-e.y;for(var h=a.length,i=1;h>i;i++){var g=a[i],j=4*i;b=i1&&(k=1);var l=Math.sqrt(e.x*e.x+e.y*e.y),m=this.texture.height/2;e.x/=l,e.y/=l,e.x*=m,e.y*=m,c[j]=g.x+e.x,c[j+1]=g.y+e.y,c[j+2]=g.x-e.x,c[j+3]=g.y-e.y,d=g}f.DisplayObjectContainer.prototype.updateTransform.call(this)}},f.Rope.prototype.setTexture=function(a){this.texture=a,this.updateFrame=!0},f.TilingSprite=function(a,b,c){f.DisplayObjectContainer.call(this),this.texture=a,this.width=b,this.height=c,this.tileScale=new f.Point(1,1),this.tilePosition=new f.Point(0,0),this.renderable=!0,this.blendMode=f.blendModes.NORMAL},f.TilingSprite.prototype=Object.create(f.DisplayObjectContainer.prototype),f.TilingSprite.prototype.constructor=f.TilingSprite,f.TilingSprite.prototype.setTexture=function(a){this.texture=a,this.updateFrame=!0},f.TilingSprite.prototype.onTextureUpdate=function(){this.updateFrame=!0},f.Spine=function(a){if(f.DisplayObjectContainer.call(this),this.spineData=f.AnimCache[a],!this.spineData)throw new Error("Spine data must be preloaded using PIXI.SpineLoader or PIXI.AssetLoader: "+a);this.skeleton=new l.Skeleton(this.spineData),this.skeleton.updateWorldTransform(),this.stateData=new l.AnimationStateData(this.spineData),this.state=new l.AnimationState(this.stateData),this.slotContainers=[];for(var b=0,c=this.skeleton.drawOrder.length;c>b;b++){var d=this.skeleton.drawOrder[b],e=d.attachment,g=new f.DisplayObjectContainer;if(this.slotContainers.push(g),this.addChild(g),e instanceof l.RegionAttachment){var h=e.rendererObject.name,i=this.createSprite(d,e.rendererObject);d.currentSprite=i,d.currentSpriteName=h,g.addChild(i)}}},f.Spine.prototype=Object.create(f.DisplayObjectContainer.prototype),f.Spine.prototype.constructor=f.Spine,f.Spine.prototype.updateTransform=function(){this.lastTime=this.lastTime||Date.now();var a=.001*(Date.now()-this.lastTime);this.lastTime=Date.now(),this.state.update(a),this.state.apply(this.skeleton),this.skeleton.updateWorldTransform();for(var b=this.skeleton.drawOrder,c=0,d=b.length;d>c;c++){var e=b[c],g=e.attachment,h=this.slotContainers[c];if(g instanceof l.RegionAttachment){if(g.rendererObject&&(!e.currentSpriteName||e.currentSpriteName!=g.name)){var i=g.rendererObject.name;if(void 0!==e.currentSprite&&(e.currentSprite.visible=!1),e.sprites=e.sprites||{},void 0!==e.sprites[i])e.sprites[i].visible=!0;else{var j=this.createSprite(e,g.rendererObject);h.addChild(j)}e.currentSprite=e.sprites[i],e.currentSpriteName=i}h.visible=!0;var k=e.bone;h.position.x=k.worldX+g.x*k.m00+g.y*k.m01,h.position.y=k.worldY+g.x*k.m10+g.y*k.m11,h.scale.x=k.worldScaleX,h.scale.y=k.worldScaleY,h.rotation=-(e.bone.worldRotation*Math.PI/180)}else h.visible=!1}f.DisplayObjectContainer.prototype.updateTransform.call(this)},f.Spine.prototype.createSprite=function(a,b){var c=f.TextureCache[b.name]?b.name:b.name+".png",d=new f.Sprite(f.Texture.fromFrame(c));return d.scale=b.scale,d.rotation=b.rotation,d.anchor.x=d.anchor.y=.5,a.sprites=a.sprites||{},a.sprites[b.name]=d,d};var l={};l.BoneData=function(a,b){this.name=a,this.parent=b},l.BoneData.prototype={length:0,x:0,y:0,rotation:0,scaleX:1,scaleY:1},l.SlotData=function(a,b){this.name=a,this.boneData=b},l.SlotData.prototype={r:1,g:1,b:1,a:1,attachmentName:null},l.Bone=function(a,b){this.data=a,this.parent=b,this.setToSetupPose()},l.Bone.yDown=!1,l.Bone.prototype={x:0,y:0,rotation:0,scaleX:1,scaleY:1,m00:0,m01:0,worldX:0,m10:0,m11:0,worldY:0,worldRotation:0,worldScaleX:1,worldScaleY:1,updateWorldTransform:function(a,b){var c=this.parent;null!=c?(this.worldX=this.x*c.m00+this.y*c.m01+c.worldX,this.worldY=this.x*c.m10+this.y*c.m11+c.worldY,this.worldScaleX=c.worldScaleX*this.scaleX,this.worldScaleY=c.worldScaleY*this.scaleY,this.worldRotation=c.worldRotation+this.rotation):(this.worldX=this.x,this.worldY=this.y,this.worldScaleX=this.scaleX,this.worldScaleY=this.scaleY,this.worldRotation=this.rotation);var d=this.worldRotation*Math.PI/180,e=Math.cos(d),f=Math.sin(d);this.m00=e*this.worldScaleX,this.m10=f*this.worldScaleX,this.m01=-f*this.worldScaleY,this.m11=e*this.worldScaleY,a&&(this.m00=-this.m00,this.m01=-this.m01),b&&(this.m10=-this.m10,this.m11=-this.m11),l.Bone.yDown&&(this.m10=-this.m10,this.m11=-this.m11)},setToSetupPose:function(){var a=this.data;this.x=a.x,this.y=a.y,this.rotation=a.rotation,this.scaleX=a.scaleX,this.scaleY=a.scaleY}},l.Slot=function(a,b,c){this.data=a,this.skeleton=b,this.bone=c,this.setToSetupPose()},l.Slot.prototype={r:1,g:1,b:1,a:1,_attachmentTime:0,attachment:null,setAttachment:function(a){this.attachment=a,this._attachmentTime=this.skeleton.time},setAttachmentTime:function(a){this._attachmentTime=this.skeleton.time-a},getAttachmentTime:function(){return this.skeleton.time-this._attachmentTime},setToSetupPose:function(){var a=this.data;this.r=a.r,this.g=a.g,this.b=a.b,this.a=a.a;for(var b=this.skeleton.data.slots,c=0,d=b.length;d>c;c++)if(b[c]==a){this.setAttachment(a.attachmentName?this.skeleton.getAttachmentBySlotIndex(c,a.attachmentName):null);break}}},l.Skin=function(a){this.name=a,this.attachments={}},l.Skin.prototype={addAttachment:function(a,b,c){this.attachments[a+":"+b]=c},getAttachment:function(a,b){return this.attachments[a+":"+b]},_attachAll:function(a,b){for(var c in b.attachments){var d=c.indexOf(":"),e=parseInt(c.substring(0,d)),f=c.substring(d+1),g=a.slots[e];if(g.attachment&&g.attachment.name==f){var h=this.getAttachment(e,f);h&&g.setAttachment(h)}}}},l.Animation=function(a,b,c){this.name=a,this.timelines=b,this.duration=c},l.Animation.prototype={apply:function(a,b,c){c&&0!=this.duration&&(b%=this.duration);for(var d=this.timelines,e=0,f=d.length;f>e;e++)d[e].apply(a,b,1)},mix:function(a,b,c,d){c&&0!=this.duration&&(b%=this.duration);for(var e=this.timelines,f=0,g=e.length;g>f;f++)e[f].apply(a,b,d)}},l.binarySearch=function(a,b,c){var d=0,e=Math.floor(a.length/c)-2;if(0==e)return c;for(var f=e>>>1;;){if(a[(f+1)*c]<=b?d=f+1:e=f,d==e)return(d+1)*c;f=d+e>>>1}},l.linearSearch=function(a,b,c){for(var d=0,e=a.length-c;e>=d;d+=c)if(a[d]>b)return d;return-1},l.Curves=function(a){this.curves=[],this.curves.length=6*(a-1)},l.Curves.prototype={setLinear:function(a){this.curves[6*a]=0},setStepped:function(a){this.curves[6*a]=-1},setCurve:function(a,b,c,d,e){var f=.1,g=f*f,h=g*f,i=3*f,j=3*g,k=6*g,l=6*h,m=2*-b+d,n=2*-c+e,o=3*(b-d)+1,p=3*(c-e)+1,q=6*a,r=this.curves;r[q]=b*i+m*j+o*h,r[q+1]=c*i+n*j+p*h,r[q+2]=m*k+o*l,r[q+3]=n*k+p*l,r[q+4]=o*l,r[q+5]=p*l},getCurvePercent:function(a,b){b=0>b?0:b>1?1:b;var c=6*a,d=this.curves,e=d[c];if(!e)return b;if(-1==e)return 0;for(var f=d[c+1],g=d[c+2],h=d[c+3],i=d[c+4],j=d[c+5],k=e,l=f,m=8;;){if(k>=b){var n=k-e,o=l-f;return o+(l-o)*(b-n)/(k-n)}if(0==m)break;m--,e+=g,f+=h,g+=i,h+=j,k+=e,l+=f}return l+(1-l)*(b-k)/(1-k)}},l.RotateTimeline=function(a){this.curves=new l.Curves(a),this.frames=[],this.frames.length=2*a},l.RotateTimeline.prototype={boneIndex:0,getFrameCount:function(){return this.frames.length/2},setFrame:function(a,b,c){a*=2,this.frames[a]=b,this.frames[a+1]=c},apply:function(a,b,c){var d=this.frames;if(!(b=d[d.length-2]){for(var f=e.data.rotation+d[d.length-1]-e.rotation;f>180;)f-=360;for(;-180>f;)f+=360;return e.rotation+=f*c,void 0}var g=l.binarySearch(d,b,2),h=d[g-1],i=d[g],j=1-(b-i)/(d[g-2]-i);j=this.curves.getCurvePercent(g/2-1,j);for(var f=d[g+1]-h;f>180;)f-=360;for(;-180>f;)f+=360;for(f=e.data.rotation+(h+f*j)-e.rotation;f>180;)f-=360;for(;-180>f;)f+=360;e.rotation+=f*c}}},l.TranslateTimeline=function(a){this.curves=new l.Curves(a),this.frames=[],this.frames.length=3*a},l.TranslateTimeline.prototype={boneIndex:0,getFrameCount:function(){return this.frames.length/3},setFrame:function(a,b,c,d){a*=3,this.frames[a]=b,this.frames[a+1]=c,this.frames[a+2]=d},apply:function(a,b,c){var d=this.frames;if(!(b=d[d.length-3])return e.x+=(e.data.x+d[d.length-2]-e.x)*c,e.y+=(e.data.y+d[d.length-1]-e.y)*c,void 0;var f=l.binarySearch(d,b,3),g=d[f-2],h=d[f-1],i=d[f],j=1-(b-i)/(d[f+-3]-i);j=this.curves.getCurvePercent(f/3-1,j),e.x+=(e.data.x+g+(d[f+1]-g)*j-e.x)*c,e.y+=(e.data.y+h+(d[f+2]-h)*j-e.y)*c}}},l.ScaleTimeline=function(a){this.curves=new l.Curves(a),this.frames=[],this.frames.length=3*a},l.ScaleTimeline.prototype={boneIndex:0,getFrameCount:function(){return this.frames.length/3},setFrame:function(a,b,c,d){a*=3,this.frames[a]=b,this.frames[a+1]=c,this.frames[a+2]=d},apply:function(a,b,c){var d=this.frames;if(!(b=d[d.length-3])return e.scaleX+=(e.data.scaleX-1+d[d.length-2]-e.scaleX)*c,e.scaleY+=(e.data.scaleY-1+d[d.length-1]-e.scaleY)*c,void 0;var f=l.binarySearch(d,b,3),g=d[f-2],h=d[f-1],i=d[f],j=1-(b-i)/(d[f+-3]-i);j=this.curves.getCurvePercent(f/3-1,j),e.scaleX+=(e.data.scaleX-1+g+(d[f+1]-g)*j-e.scaleX)*c,e.scaleY+=(e.data.scaleY-1+h+(d[f+2]-h)*j-e.scaleY)*c}}},l.ColorTimeline=function(a){this.curves=new l.Curves(a),this.frames=[],this.frames.length=5*a},l.ColorTimeline.prototype={slotIndex:0,getFrameCount:function(){return this.frames.length/2},setFrame:function(c,d){c*=5,this.frames[c]=d,this.frames[c+1]=r,this.frames[c+2]=g,this.frames[c+3]=b,this.frames[c+4]=a},apply:function(a,b,c){var d=this.frames;if(!(b=d[d.length-5]){var f=d.length-1;return e.r=d[f-3],e.g=d[f-2],e.b=d[f-1],e.a=d[f],void 0}var g=l.binarySearch(d,b,5),h=d[g-4],i=d[g-3],j=d[g-2],k=d[g-1],m=d[g],n=1-(b-m)/(d[g-5]-m);n=this.curves.getCurvePercent(g/5-1,n);var o=h+(d[g+1]-h)*n,p=i+(d[g+2]-i)*n,q=j+(d[g+3]-j)*n,r=k+(d[g+4]-k)*n;1>c?(e.r+=(o-e.r)*c,e.g+=(p-e.g)*c,e.b+=(q-e.b)*c,e.a+=(r-e.a)*c):(e.r=o,e.g=p,e.b=q,e.a=r)}}},l.AttachmentTimeline=function(a){this.curves=new l.Curves(a),this.frames=[],this.frames.length=a,this.attachmentNames=[],this.attachmentNames.length=a},l.AttachmentTimeline.prototype={slotIndex:0,getFrameCount:function(){return this.frames.length},setFrame:function(a,b,c){this.frames[a]=b,this.attachmentNames[a]=c},apply:function(a,b){var c=this.frames;if(!(b=c[c.length-1]?c.length-1:l.binarySearch(c,b,1)-1;var e=this.attachmentNames[d];a.slots[this.slotIndex].setAttachment(e?a.getAttachmentBySlotIndex(this.slotIndex,e):null)}}},l.SkeletonData=function(){this.bones=[],this.slots=[],this.skins=[],this.animations=[]},l.SkeletonData.prototype={defaultSkin:null,findBone:function(a){for(var b=this.bones,c=0,d=b.length;d>c;c++)if(b[c].name==a)return b[c];return null},findBoneIndex:function(a){for(var b=this.bones,c=0,d=b.length;d>c;c++)if(b[c].name==a)return c;return-1},findSlot:function(a){for(var b=this.slots,c=0,d=b.length;d>c;c++)if(b[c].name==a)return slot[c];return null},findSlotIndex:function(a){for(var b=this.slots,c=0,d=b.length;d>c;c++)if(b[c].name==a)return c;return-1},findSkin:function(a){for(var b=this.skins,c=0,d=b.length;d>c;c++)if(b[c].name==a)return b[c];return null},findAnimation:function(a){for(var b=this.animations,c=0,d=b.length;d>c;c++)if(b[c].name==a)return b[c];return null}},l.Skeleton=function(a){this.data=a,this.bones=[];for(var b=0,c=a.bones.length;c>b;b++){var d=a.bones[b],e=d.parent?this.bones[a.bones.indexOf(d.parent)]:null;this.bones.push(new l.Bone(d,e))}this.slots=[],this.drawOrder=[];for(var b=0,c=a.slots.length;c>b;b++){var f=a.slots[b],g=this.bones[a.bones.indexOf(f.boneData)],h=new l.Slot(f,this,g);this.slots.push(h),this.drawOrder.push(h)}},l.Skeleton.prototype={x:0,y:0,skin:null,r:1,g:1,b:1,a:1,time:0,flipX:!1,flipY:!1,updateWorldTransform:function(){for(var a=this.flipX,b=this.flipY,c=this.bones,d=0,e=c.length;e>d;d++)c[d].updateWorldTransform(a,b)},setToSetupPose:function(){this.setBonesToSetupPose(),this.setSlotsToSetupPose()},setBonesToSetupPose:function(){for(var a=this.bones,b=0,c=a.length;c>b;b++)a[b].setToSetupPose()},setSlotsToSetupPose:function(){for(var a=this.slots,b=0,c=a.length;c>b;b++)a[b].setToSetupPose(b)},getRootBone:function(){return 0==this.bones.length?null:this.bones[0]},findBone:function(a){for(var b=this.bones,c=0,d=b.length;d>c;c++)if(b[c].data.name==a)return b[c];return null},findBoneIndex:function(a){for(var b=this.bones,c=0,d=b.length;d>c;c++)if(b[c].data.name==a)return c;return-1},findSlot:function(a){for(var b=this.slots,c=0,d=b.length;d>c;c++)if(b[c].data.name==a)return b[c];return null},findSlotIndex:function(a){for(var b=this.slots,c=0,d=b.length;d>c;c++)if(b[c].data.name==a)return c;return-1},setSkinByName:function(a){var b=this.data.findSkin(a);if(!b)throw"Skin not found: "+a;this.setSkin(b)},setSkin:function(a){this.skin&&a&&a._attachAll(this,this.skin),this.skin=a},getAttachmentBySlotName:function(a,b){return this.getAttachmentBySlotIndex(this.data.findSlotIndex(a),b)},getAttachmentBySlotIndex:function(a,b){if(this.skin){var c=this.skin.getAttachment(a,b);if(c)return c}return this.data.defaultSkin?this.data.defaultSkin.getAttachment(a,b):null},setAttachment:function(a,b){for(var c=this.slots,d=0,e=c.size;e>d;d++){var f=c[d];if(f.data.name==a){var g=null;if(b&&(g=this.getAttachment(d,b),null==g))throw"Attachment not found: "+b+", for slot: "+a;return f.setAttachment(g),void 0}}throw"Slot not found: "+a},update:function(a){time+=a}},l.AttachmentType={region:0},l.RegionAttachment=function(){this.offset=[],this.offset.length=8,this.uvs=[],this.uvs.length=8},l.RegionAttachment.prototype={x:0,y:0,rotation:0,scaleX:1,scaleY:1,width:0,height:0,rendererObject:null,regionOffsetX:0,regionOffsetY:0,regionWidth:0,regionHeight:0,regionOriginalWidth:0,regionOriginalHeight:0,setUVs:function(a,b,c,d,e){var f=this.uvs;e?(f[2]=a,f[3]=d,f[4]=a,f[5]=b,f[6]=c,f[7]=b,f[0]=c,f[1]=d):(f[0]=a,f[1]=d,f[2]=a,f[3]=b,f[4]=c,f[5]=b,f[6]=c,f[7]=d)},updateOffset:function(){var a=this.width/this.regionOriginalWidth*this.scaleX,b=this.height/this.regionOriginalHeight*this.scaleY,c=-this.width/2*this.scaleX+this.regionOffsetX*a,d=-this.height/2*this.scaleY+this.regionOffsetY*b,e=c+this.regionWidth*a,f=d+this.regionHeight*b,g=this.rotation*Math.PI/180,h=Math.cos(g),i=Math.sin(g),j=c*h+this.x,k=c*i,l=d*h+this.y,m=d*i,n=e*h+this.x,o=e*i,p=f*h+this.y,q=f*i,r=this.offset;r[0]=j-m,r[1]=l+k,r[2]=j-q,r[3]=p+k,r[4]=n-q,r[5]=p+o,r[6]=n-m,r[7]=l+o},computeVertices:function(a,b,c,d){a+=c.worldX,b+=c.worldY;var e=c.m00,f=c.m01,g=c.m10,h=c.m11,i=this.offset;d[0]=i[0]*e+i[1]*f+a,d[1]=i[0]*g+i[1]*h+b,d[2]=i[2]*e+i[3]*f+a,d[3]=i[2]*g+i[3]*h+b,d[4]=i[4]*e+i[5]*f+a,d[5]=i[4]*g+i[5]*h+b,d[6]=i[6]*e+i[7]*f+a,d[7]=i[6]*g+i[7]*h+b}},l.AnimationStateData=function(a){this.skeletonData=a,this.animationToMixTime={}},l.AnimationStateData.prototype={defaultMix:0,setMixByName:function(a,b,c){var d=this.skeletonData.findAnimation(a);if(!d)throw"Animation not found: "+a;var e=this.skeletonData.findAnimation(b);if(!e)throw"Animation not found: "+b;this.setMix(d,e,c)},setMix:function(a,b,c){this.animationToMixTime[a.name+":"+b.name]=c},getMix:function(a,b){var c=this.animationToMixTime[a.name+":"+b.name];return c?c:this.defaultMix}},l.AnimationState=function(a){this.data=a,this.queue=[]},l.AnimationState.prototype={current:null,previous:null,currentTime:0,previousTime:0,currentLoop:!1,previousLoop:!1,mixTime:0,mixDuration:0,update:function(a){if(this.currentTime+=a,this.previousTime+=a,this.mixTime+=a,this.queue.length>0){var b=this.queue[0];this.currentTime>=b.delay&&(this._setAnimation(b.animation,b.loop),this.queue.shift())}},apply:function(a){if(this.current)if(this.previous){this.previous.apply(a,this.previousTime,this.previousLoop);var b=this.mixTime/this.mixDuration;b>=1&&(b=1,this.previous=null),this.current.mix(a,this.currentTime,this.currentLoop,b)}else this.current.apply(a,this.currentTime,this.currentLoop)},clearAnimation:function(){this.previous=null,this.current=null,this.queue.length=0},_setAnimation:function(a,b){this.previous=null,a&&this.current&&(this.mixDuration=this.data.getMix(this.current,a),this.mixDuration>0&&(this.mixTime=0,this.previous=this.current,this.previousTime=this.currentTime,this.previousLoop=this.currentLoop)),this.current=a,this.currentLoop=b,this.currentTime=0},setAnimationByName:function(a,b){var c=this.data.skeletonData.findAnimation(a);if(!c)throw"Animation not found: "+a;this.setAnimation(c,b)},setAnimation:function(a,b){this.queue.length=0,this._setAnimation(a,b)},addAnimationByName:function(a,b,c){var d=this.data.skeletonData.findAnimation(a);if(!d)throw"Animation not found: "+a;this.addAnimation(d,b,c)},addAnimation:function(a,b,c){var d={};if(d.animation=a,d.loop=b,!c||0>=c){var e=0==this.queue.length?this.current:this.queue[this.queue.length-1].animation;c=null!=e?e.duration-this.data.getMix(e,a)+(c||0):0}d.delay=c,this.queue.push(d)},isComplete:function(){return!this.current||this.currentTime>=this.current.duration}},l.SkeletonJson=function(a){this.attachmentLoader=a},l.SkeletonJson.prototype={scale:1,readSkeletonData:function(a){for(var b=new l.SkeletonData,c=a.bones,d=0,e=c.length;e>d;d++){var f=c[d],g=null;if(f.parent&&(g=b.findBone(f.parent),!g))throw"Parent bone not found: "+f.parent;var h=new l.BoneData(f.name,g);h.length=(f.length||0)*this.scale,h.x=(f.x||0)*this.scale,h.y=(f.y||0)*this.scale,h.rotation=f.rotation||0,h.scaleX=f.scaleX||1,h.scaleY=f.scaleY||1,b.bones.push(h)}for(var i=a.slots,d=0,e=i.length;e>d;d++){var j=i[d],h=b.findBone(j.bone);if(!h)throw"Slot bone not found: "+j.bone;var k=new l.SlotData(j.name,h),m=j.color;m&&(k.r=l.SkeletonJson.toColor(m,0),k.g=l.SkeletonJson.toColor(m,1),k.b=l.SkeletonJson.toColor(m,2),k.a=l.SkeletonJson.toColor(m,3)),k.attachmentName=j.attachment,b.slots.push(k)}var n=a.skins;for(var o in n)if(n.hasOwnProperty(o)){var p=n[o],q=new l.Skin(o);for(var r in p)if(p.hasOwnProperty(r)){var s=b.findSlotIndex(r),t=p[r];for(var u in t)if(t.hasOwnProperty(u)){var v=this.readAttachment(q,u,t[u]);null!=v&&q.addAttachment(s,u,v)}}b.skins.push(q),"default"==q.name&&(b.defaultSkin=q)}var w=a.animations;for(var x in w)w.hasOwnProperty(x)&&this.readAnimation(x,w[x],b);return b},readAttachment:function(a,b,c){b=c.name||b;var d=l.AttachmentType[c.type||"region"];if(d==l.AttachmentType.region){var e=new l.RegionAttachment;return e.x=(c.x||0)*this.scale,e.y=(c.y||0)*this.scale,e.scaleX=c.scaleX||1,e.scaleY=c.scaleY||1,e.rotation=c.rotation||0,e.width=(c.width||32)*this.scale,e.height=(c.height||32)*this.scale,e.updateOffset(),e.rendererObject={},e.rendererObject.name=b,e.rendererObject.scale={},e.rendererObject.scale.x=e.scaleX,e.rendererObject.scale.y=e.scaleY,e.rendererObject.rotation=-e.rotation*Math.PI/180,e}throw"Unknown attachment type: "+d},readAnimation:function(a,b,c){var d=[],e=0,f=b.bones;for(var g in f)if(f.hasOwnProperty(g)){var h=c.findBoneIndex(g);if(-1==h)throw"Bone not found: "+g;var i=f[g];for(var j in i)if(i.hasOwnProperty(j)){var k=i[j];if("rotate"==j){var m=new l.RotateTimeline(k.length);m.boneIndex=h;for(var n=0,o=0,p=k.length;p>o;o++){var q=k[o];m.setFrame(n,q.time,q.angle),l.SkeletonJson.readCurve(m,n,q),n++}d.push(m),e=Math.max(e,m.frames[2*m.getFrameCount()-2])}else{if("translate"!=j&&"scale"!=j)throw"Invalid timeline type for a bone: "+j+" ("+g+")";var m,r=1;"scale"==j?m=new l.ScaleTimeline(k.length):(m=new l.TranslateTimeline(k.length),r=this.scale),m.boneIndex=h;for(var n=0,o=0,p=k.length;p>o;o++){var q=k[o],s=(q.x||0)*r,t=(q.y||0)*r;m.setFrame(n,q.time,s,t),l.SkeletonJson.readCurve(m,n,q),n++}d.push(m),e=Math.max(e,m.frames[3*m.getFrameCount()-3])}}}var u=b.slots;for(var v in u)if(u.hasOwnProperty(v)){var w=u[v],x=c.findSlotIndex(v);for(var j in w)if(w.hasOwnProperty(j)){var k=w[j];if("color"==j){var m=new l.ColorTimeline(k.length);m.slotIndex=x;for(var n=0,o=0,p=k.length;p>o;o++){var q=k[o],y=q.color,z=l.SkeletonJson.toColor(y,0),A=l.SkeletonJson.toColor(y,1),B=l.SkeletonJson.toColor(y,2),C=l.SkeletonJson.toColor(y,3);m.setFrame(n,q.time,z,A,B,C),l.SkeletonJson.readCurve(m,n,q),n++}d.push(m),e=Math.max(e,m.frames[5*m.getFrameCount()-5])}else{if("attachment"!=j)throw"Invalid timeline type for a slot: "+j+" ("+v+")";var m=new l.AttachmentTimeline(k.length);m.slotIndex=x;for(var n=0,o=0,p=k.length;p>o;o++){var q=k[o];m.setFrame(n++,q.time,q.name)}d.push(m),e=Math.max(e,m.frames[m.getFrameCount()-1])}}}c.animations.push(new l.Animation(a,d,e))}},l.SkeletonJson.readCurve=function(a,b,c){var d=c.curve;d&&("stepped"==d?a.curves.setStepped(b):d instanceof Array&&a.curves.setCurve(b,d[0],d[1],d[2],d[3]))},l.SkeletonJson.toColor=function(a,b){if(8!=a.length)throw"Color hexidecimal length must be 8, recieved: "+a;return parseInt(a.substring(2*b,2),16)/255},l.Atlas=function(a,b){this.textureLoader=b,this.pages=[],this.regions=[];var c=new l.AtlasReader(a),d=[];d.length=4;for(var e=null;;){var f=c.readLine();if(null==f)break;if(f=c.trim(f),0==f.length)e=null;else if(e){var g=new l.AtlasRegion;g.name=f,g.page=e,g.rotate="true"==c.readValue(),c.readTuple(d);var h=parseInt(d[0]),i=parseInt(d[1]);c.readTuple(d);var j=parseInt(d[0]),k=parseInt(d[1]);g.u=h/e.width,g.v=i/e.height,g.rotate?(g.u2=(h+k)/e.width,g.v2=(i+j)/e.height):(g.u2=(h+j)/e.width,g.v2=(i+k)/e.height),g.x=h,g.y=i,g.width=Math.abs(j),g.height=Math.abs(k),4==c.readTuple(d)&&(g.splits=[parseInt(d[0]),parseInt(d[1]),parseInt(d[2]),parseInt(d[3])],4==c.readTuple(d)&&(g.pads=[parseInt(d[0]),parseInt(d[1]),parseInt(d[2]),parseInt(d[3])],c.readTuple(d))),g.originalWidth=parseInt(d[0]),g.originalHeight=parseInt(d[1]),c.readTuple(d),g.offsetX=parseInt(d[0]),g.offsetY=parseInt(d[1]),g.index=parseInt(c.readValue()),this.regions.push(g)}else{e=new l.AtlasPage,e.name=f,e.format=l.Atlas.Format[c.readValue()],c.readTuple(d),e.minFilter=l.Atlas.TextureFilter[d[0]],e.magFilter=l.Atlas.TextureFilter[d[1]];var m=c.readValue();e.uWrap=l.Atlas.TextureWrap.clampToEdge,e.vWrap=l.Atlas.TextureWrap.clampToEdge,"x"==m?e.uWrap=l.Atlas.TextureWrap.repeat:"y"==m?e.vWrap=l.Atlas.TextureWrap.repeat:"xy"==m&&(e.uWrap=e.vWrap=l.Atlas.TextureWrap.repeat),b.load(e,f),this.pages.push(e)}}},l.Atlas.prototype={findRegion:function(a){for(var b=this.regions,c=0,d=b.length;d>c;c++)if(b[c].name==a)return b[c];return null},dispose:function(){for(var a=this.pages,b=0,c=a.length;c>b;b++)this.textureLoader.unload(a[b].rendererObject)},updateUVs:function(a){for(var b=this.regions,c=0,d=b.length;d>c;c++){var e=b[c];e.page==a&&(e.u=e.x/a.width,e.v=e.y/a.height,e.rotate?(e.u2=(e.x+e.height)/a.width,e.v2=(e.y+e.width)/a.height):(e.u2=(e.x+e.width)/a.width,e.v2=(e.y+e.height)/a.height))}}},l.Atlas.Format={alpha:0,intensity:1,luminanceAlpha:2,rgb565:3,rgba4444:4,rgb888:5,rgba8888:6},l.Atlas.TextureFilter={nearest:0,linear:1,mipMap:2,mipMapNearestNearest:3,mipMapLinearNearest:4,mipMapNearestLinear:5,mipMapLinearLinear:6},l.Atlas.TextureWrap={mirroredRepeat:0,clampToEdge:1,repeat:2},l.AtlasPage=function(){},l.AtlasPage.prototype={name:null,format:null,minFilter:null,magFilter:null,uWrap:null,vWrap:null,rendererObject:null,width:0,height:0},l.AtlasRegion=function(){},l.AtlasRegion.prototype={page:null,name:null,x:0,y:0,width:0,height:0,u:0,v:0,u2:0,v2:0,offsetX:0,offsetY:0,originalWidth:0,originalHeight:0,index:0,rotate:!1,splits:null,pads:null},l.AtlasReader=function(a){this.lines=a.split(/\r\n|\r|\n/)},l.AtlasReader.prototype={index:0,trim:function(a){return a.replace(/^\s+|\s+$/g,"")},readLine:function(){return this.index>=this.lines.length?null:this.lines[this.index++]},readValue:function(){var a=this.readLine(),b=a.indexOf(":");if(-1==b)throw"Invalid line: "+a;return this.trim(a.substring(b+1))},readTuple:function(a){var b=this.readLine(),c=b.indexOf(":");if(-1==c)throw"Invalid line: "+b;for(var d=0,e=c+1;3>d;d++){var f=b.indexOf(",",e);if(-1==f){if(0==d)throw"Invalid line: "+b;break}a[d]=this.trim(b.substr(e,f-e)),e=f+1}return a[d]=this.trim(b.substring(e)),d+1}},l.AtlasAttachmentLoader=function(a){this.atlas=a},l.AtlasAttachmentLoader.prototype={newAttachment:function(a,b,c){switch(b){case l.AttachmentType.region:var d=this.atlas.findRegion(c);if(!d)throw"Region not found in atlas: "+c+" ("+b+")";var e=new l.RegionAttachment(c);return e.rendererObject=d,e.setUVs(d.u,d.v,d.u2,d.v2,d.rotate),e.regionOffsetX=d.offsetX,e.regionOffsetY=d.offsetY,e.regionWidth=d.width,e.regionHeight=d.height,e.regionOriginalWidth=d.originalWidth,e.regionOriginalHeight=d.originalHeight,e}throw"Unknown attachment type: "+b}},f.AnimCache={},l.Bone.yDown=!0,f.CustomRenderable=function(){f.DisplayObject.call(this)},f.CustomRenderable.prototype=Object.create(f.DisplayObject.prototype),f.CustomRenderable.prototype.constructor=f.CustomRenderable,f.CustomRenderable.prototype.renderCanvas=function(){},f.CustomRenderable.prototype.initWebGL=function(){},f.CustomRenderable.prototype.renderWebGL=function(){},f.BaseTextureCache={},f.texturesToUpdate=[],f.texturesToDestroy=[],f.BaseTexture=function(a){if(f.EventTarget.call(this),this.width=100,this.height=100,this.hasLoaded=!1,this.source=a,a){if(this.source instanceof Image||this.source instanceof HTMLImageElement)if(this.source.complete)this.hasLoaded=!0,this.width=this.source.width,this.height=this.source.height,f.texturesToUpdate.push(this);else{var b=this;this.source.onload=function(){b.hasLoaded=!0,b.width=b.source.width,b.height=b.source.height,f.texturesToUpdate.push(b),b.dispatchEvent({type:"loaded",content:b})}}else this.hasLoaded=!0,this.width=this.source.width,this.height=this.source.height,f.texturesToUpdate.push(this);this._powerOf2=!1}},f.BaseTexture.prototype.constructor=f.BaseTexture,f.BaseTexture.prototype.destroy=function(){this.source instanceof Image&&(this.source.src=null),this.source=null,f.texturesToDestroy.push(this)},f.BaseTexture.fromImage=function(a,b){var c=f.BaseTextureCache[a];if(!c){var d=new Image;b&&(d.crossOrigin=""),d.src=a,c=new f.BaseTexture(d),f.BaseTextureCache[a]=c}return c},f.TextureCache={},f.FrameCache={},f.Texture=function(a,b){if(f.EventTarget.call(this),b||(this.noFrame=!0,b=new f.Rectangle(0,0,1,1)),a instanceof f.Texture&&(a=a.baseTexture),this.baseTexture=a,this.frame=b,this.trim=new f.Point,this.scope=this,a.hasLoaded)this.noFrame&&(b=new f.Rectangle(0,0,a.width,a.height)),this.setFrame(b);else{var c=this;a.addEventListener("loaded",function(){c.onBaseTextureLoaded()})}},f.Texture.prototype.constructor=f.Texture,f.Texture.prototype.onBaseTextureLoaded=function(){var a=this.baseTexture;a.removeEventListener("loaded",this.onLoaded),this.noFrame&&(this.frame=new f.Rectangle(0,0,a.width,a.height)),this.noFrame=!1,this.width=this.frame.width,this.height=this.frame.height,this.scope.dispatchEvent({type:"update",content:this})},f.Texture.prototype.destroy=function(a){a&&this.baseTexture.destroy()},f.Texture.prototype.setFrame=function(a){if(this.frame=a,this.width=a.width,this.height=a.height,a.x+a.width>this.baseTexture.width||a.y+a.height>this.baseTexture.height)throw new Error("Texture Error: frame does not fit inside the base Texture dimensions "+this);this.updateFrame=!0,f.Texture.frameUpdates.push(this)},f.Texture.fromImage=function(a,b){var c=f.TextureCache[a];return c||(c=new f.Texture(f.BaseTexture.fromImage(a,b)),f.TextureCache[a]=c),c},f.Texture.fromFrame=function(a){var b=f.TextureCache[a];if(!b)throw new Error("The frameId '"+a+"' does not exist in the texture cache "+this);return b},f.Texture.fromCanvas=function(a){var b=new f.BaseTexture(a);return new f.Texture(b)},f.Texture.addTextureToCache=function(a,b){f.TextureCache[b]=a},f.Texture.removeTextureFromCache=function(a){var b=f.TextureCache[a];return f.TextureCache[a]=null,b},f.Texture.frameUpdates=[],f.RenderTexture=function(a,b){f.EventTarget.call(this),this.width=a||100,this.height=b||100,this.indetityMatrix=f.mat3.create(),this.frame=new f.Rectangle(0,0,this.width,this.height),f.gl?this.initWebGL():this.initCanvas()},f.RenderTexture.prototype=Object.create(f.Texture.prototype),f.RenderTexture.prototype.constructor=f.RenderTexture,f.RenderTexture.prototype.initWebGL=function(){var a=f.gl;this.glFramebuffer=a.createFramebuffer(),a.bindFramebuffer(a.FRAMEBUFFER,this.glFramebuffer),this.glFramebuffer.width=this.width,this.glFramebuffer.height=this.height,this.baseTexture=new f.BaseTexture,this.baseTexture.width=this.width,this.baseTexture.height=this.height,this.baseTexture._glTexture=a.createTexture(),a.bindTexture(a.TEXTURE_2D,this.baseTexture._glTexture),a.texImage2D(a.TEXTURE_2D,0,a.RGBA,this.width,this.height,0,a.RGBA,a.UNSIGNED_BYTE,null),a.texParameteri(a.TEXTURE_2D,a.TEXTURE_MAG_FILTER,a.LINEAR),a.texParameteri(a.TEXTURE_2D,a.TEXTURE_MIN_FILTER,a.LINEAR),a.texParameteri(a.TEXTURE_2D,a.TEXTURE_WRAP_S,a.CLAMP_TO_EDGE),a.texParameteri(a.TEXTURE_2D,a.TEXTURE_WRAP_T,a.CLAMP_TO_EDGE),this.baseTexture.isRender=!0,a.bindFramebuffer(a.FRAMEBUFFER,this.glFramebuffer),a.framebufferTexture2D(a.FRAMEBUFFER,a.COLOR_ATTACHMENT0,a.TEXTURE_2D,this.baseTexture._glTexture,0),this.projection=new f.Point(this.width/2,this.height/2),this.render=this.renderWebGL +},f.RenderTexture.prototype.resize=function(a,b){if(this.width=a,this.height=b,f.gl){this.projection.x=this.width/2,this.projection.y=this.height/2;var c=f.gl;c.bindTexture(c.TEXTURE_2D,this.baseTexture._glTexture),c.texImage2D(c.TEXTURE_2D,0,c.RGBA,this.width,this.height,0,c.RGBA,c.UNSIGNED_BYTE,null)}else this.frame.width=this.width,this.frame.height=this.height,this.renderer.resize(this.width,this.height)},f.RenderTexture.prototype.initCanvas=function(){this.renderer=new f.CanvasRenderer(this.width,this.height,null,0),this.baseTexture=new f.BaseTexture(this.renderer.view),this.frame=new f.Rectangle(0,0,this.width,this.height),this.render=this.renderCanvas},f.RenderTexture.prototype.renderWebGL=function(a,b,c){var d=f.gl;d.colorMask(!0,!0,!0,!0),d.viewport(0,0,this.width,this.height),d.bindFramebuffer(d.FRAMEBUFFER,this.glFramebuffer),c&&(d.clearColor(0,0,0,0),d.clear(d.COLOR_BUFFER_BIT));var e=a.children,g=a.worldTransform;a.worldTransform=f.mat3.create(),a.worldTransform[4]=-1,a.worldTransform[5]=2*this.projection.y,b&&(a.worldTransform[2]=b.x,a.worldTransform[5]-=b.y),f.visibleCount++,a.vcount=f.visibleCount;for(var h=0,i=e.length;i>h;h++)e[h].updateTransform();var j=a.__renderGroup;j?a==j.root?j.render(this.projection):j.renderSpecific(a,this.projection):(this.renderGroup||(this.renderGroup=new f.WebGLRenderGroup(d)),this.renderGroup.setRenderable(a),this.renderGroup.render(this.projection)),a.worldTransform=g},f.RenderTexture.prototype.renderCanvas=function(a,b,c){var d=a.children;a.worldTransform=f.mat3.create(),b&&(a.worldTransform[2]=b.x,a.worldTransform[5]=b.y);for(var e=0,g=d.length;g>e;e++)d[e].updateTransform();c&&this.renderer.context.clearRect(0,0,this.width,this.height),this.renderer.renderDisplayObject(a),this.renderer.context.setTransform(1,0,0,1,0,0)},f.AssetLoader=function(a,b){f.EventTarget.call(this),this.assetURLs=a,this.crossorigin=b,this.loadersByType={jpg:f.ImageLoader,jpeg:f.ImageLoader,png:f.ImageLoader,gif:f.ImageLoader,json:f.JsonLoader,anim:f.SpineLoader,xml:f.BitmapFontLoader,fnt:f.BitmapFontLoader}},f.AssetLoader.prototype.constructor=f.AssetLoader,f.AssetLoader.prototype.load=function(){var a=this;this.loadCount=this.assetURLs.length;for(var b=0;bgrid 10x10 +balancedBinTree, depth=6 +circularLadder, length=10 +path, length=10 +complete, n=5 + +Click any of them to render corresponding graph in your browser. Use mouse wheel to zoom. Drag nodes around with left button. + +# How does this work + +First of all, [fabric.js](http://fabricjs.com/) is used as a 2d rendering engine. I created [`ngraph.fabric`](https://github.com/anvaka/ngraph.fabric) on top of it to make graph rendering easier: + +``` js +var graph = require('ngraph.graph')(); +graph.addLink(1, 2); + +var fabricGraphics = require('ngraph.fabric')(graph); + +fabricGraphics.run(); // Launch animation loop +``` + +This will render a graph with two rectangular nodes. To customize its appearance we need to tell API how we want to render nodes and where: + +``` js +var fabricGraphics = require('ngraph.fabric')(graph); +fabricGraphics.createNodeUI(function (node) { + // Now each node will be rendered as a circle + return new fabric.Circle({ radius: Math.random() * 20, fill: getNiceColor() }); + }); + +// We also want to update circle position on each frame: +fabricGraphcs.renderNode(function (circle) { + // fabric.js uses left/top to position primitives. + circle.left = circle.pos.x - circle.radius; + circle.top = circle.pos.y - circle.radius; + // `pos` property is automatically added by `ngraph.fabric` to tell where + // element should be according to layout algorithm + }); +``` + +If we look closer, all we need to customize appearance of a graph is instance of `fabricGraphics` and instance of `fabric` itself. This can be extracted into a separate file: [`ui.js`](https://github.com/anvaka/ngraph/blob/master/examples/fabric.js/Node%20and%20Browser/ui.js). Then anyone who wants to render nodes as circles with a nice color can require this file. And this is exactly what [`index.js`](https://github.com/anvaka/ngraph/blob/bfc08575ba9c0bb83387813d87c3a41f0124ecb0/examples/fabric.js/Node%20and%20Browser/index.js#L8) is doing. + +To use this from a browser we [browserify](http://browserify.org/) `index.js` and include produced script into [html file](https://github.com/anvaka/ngraph/blob/05ba2ad483409be5b5dca12624c9819306b6c51e/examples/fabric.js/Node%20and%20Browser/index.html#L9-L10). + +## How to render from node.js +`fabric.js` has two awesome parts: + +1. Rich API +2. Support of rendering from node.js + +By virtue of the last part, `ngraph.fabric` can render to static images for free. + +When rendering from node we don't need to update scene on each frame. Instead we manually calculate good layout: + +``` js +var layout = require('ngraph.forcelayout')(graph); +for (var i = 0; i < iterationsCount; ++i) { + layout.step(); +} +``` + +And initialize `ngraph.fabric` with our own layout: + +``` js +var fabricGraphics = require('ngraph.fabric')(graph, { + layout: layout +}); + +// We want custom UI from `ui.js`: +require('./ui')(fabricGraphics, fabric); + +// Ask renderer to render just one frame: +fabricGraphics.renderOneFrame(); +``` + +Finally we save `fabric.js` canvas into a file: + +``` js +var fs = require('fs'); +fabricGraphics.canvas.createPNGStream() + .pipe(fs.createWriteStream('graphFile.png')); +``` + +# Thank you for reading! + +I hope you enjoyed this little introduction into `ngraph.fabric`. Please let me know if something is not described well enough and I'll do my best to improve this :). + +[1]: https://github.com/anvaka/ngraph/blob/master/examples/fabric.js/Node%20and%20Browser/assets/create.js diff --git a/pixi/assets/balancedBinTree.png b/pixi/assets/balancedBinTree.png new file mode 100644 index 0000000000000000000000000000000000000000..fafe93d1d6a40656e53f1fc1b00a439cde081eb5 GIT binary patch literal 101495 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

;>9t+$ck8ot8PyD(K!|rF$9WLa}aARVQ>- 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 NLF%JqsklMF{{pzmIluq_ literal 0 HcmV?d00001 diff --git a/pixi/assets/circularLadder.png b/pixi/assets/circularLadder.png new file mode 100644 index 0000000000000000000000000000000000000000..392e14c63ea8efb6e3b45507a71eadea1d2d50ef GIT binary patch literal 16355 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 W*O69OuaT$r0YzCAaD}vS(EkChKP-g+ literal 0 HcmV?d00001 diff --git a/pixi/assets/complete.png b/pixi/assets/complete.png new file mode 100644 index 0000000000000000000000000000000000000000..92484883e210addad1ffe52d4ddc7a24b8c141e9 GIT binary patch literal 4306 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 literal 0 HcmV?d00001 diff --git a/pixi/assets/create.js b/pixi/assets/create.js new file mode 100644 index 0000000..0abf7e2 --- /dev/null +++ b/pixi/assets/create.js @@ -0,0 +1,57 @@ +// This file generates thumbnails for several graphs from generator. +// It duplicates lot of code from `../doItFromNode.js`. +// I tried to make `doItFromNode.js` as very simple example without splitting +// it into multiple files to show how small it is. But it +// might make more sense to just split that further... + +saveGraph('grid', {n: 10, m: 10}); +saveGraph('ladder', { n: 10 }); +saveGraph('complete', { n: 5 }); +saveGraph('balancedBinTree', {n: 6, iterationsCount: 400}); +saveGraph('path', {n: 10 }); +saveGraph('circularLadder', { n: 10 }); + +function saveGraph(graphName, options) { + var graph = require('ngraph.generators')[graphName](options.n, options.m); + + console.log('Running layout for ' + graphName); + var layout = layoutGraph(graph, options.iterationsCount); + console.log('Done. Rendering graph...'); + + var canvas = renderToCanvas(graph, layout); + saveCanvasToFile(canvas, graphName + '.png'); +} + +function layoutGraph(graph, iterationsCount) { + iterationsCount = iterationsCount || 200; + // we are going to use our own layout: + var layout = require('ngraph.forcelayout')(graph); + for (var i = 0; i < iterationsCount; ++i) { + layout.step(); + } + return layout; +} + +function renderToCanvas(graph, layout) { + var graphRect = layout.getGraphRect(); + var size = Math.max(graphRect.x2 - graphRect.x1, graphRect.y2 - graphRect.y1) + 200; + + var fabricGraphics = require('ngraph.fabric')(graph, { width: size, height: size, layout: layout }); + var fabric = require('fabric').fabric; + + require('../ui')(fabricGraphics, fabric); + + var scale = 1; + fabricGraphics.setTransform(size/2, size/2, scale); + fabricGraphics.renderOneFrame(); // One frame is enough + + return fabricGraphics.canvas; +} + +function saveCanvasToFile(canvas, fileName) { + var fs = require('fs'); + var path = require('path'); + var fullName = path.join(__dirname, fileName); + + canvas.createPNGStream().pipe(fs.createWriteStream(fullName)); +} diff --git a/pixi/assets/grid.png b/pixi/assets/grid.png new file mode 100644 index 0000000000000000000000000000000000000000..7b2c0a24c98208defb998d9360a53acdb297b45f GIT binary patch literal 93814 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 literal 0 HcmV?d00001 diff --git a/pixi/assets/ladder.png b/pixi/assets/ladder.png new file mode 100644 index 0000000000000000000000000000000000000000..c6f52851eac449e4969690b5c2630d98a2f8cb5b GIT binary patch literal 17744 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 0) { g.addLink(node, i - 1 + j * n); } + if (j > 0) { g.addLink(node, i + (j - 1) * n); } + } + } + + return g; +} + +/** + * Generates a graph in a form of a 3d grid with n rows and m columns and z levels. + * + * @param {Number} n of rows in the graph. + * @param {Number} m of columns in the graph. + * @param {Number} z of levels in the graph. + */ +function grid3(n, m, z) { + if (n < 1 || m < 1 || z < 1) { + throw new Error("Invalid number of nodes in grid3 graph"); + } + var g = createGraph(), + i, j, k; + + if (n === 1 && m === 1 && z === 1) { + g.addNode(0); + return g; + } + + for (k = 0; k < z; ++k) { + for (i = 0; i < n; ++i) { + for (j = 0; j < m; ++j) { + var level = k * n * m; + var node = i + j * n + level; + if (i > 0) { g.addLink(node, i - 1 + j * n + level); } + if (j > 0) { g.addLink(node, i + (j - 1) * n + level); } + if (k > 0) { g.addLink(node, i + j * n + (k - 1) * n * m ); } + } + } + } + + return g; +} + +/** + * Creates balanced binary tree with n levels. + * + * @param {Number} n of levels in the binary tree + */ +function balancedBinTree(n) { + if (n < 0) { + throw new Error("Invalid number of nodes in balanced tree"); + } + var g = createGraph(), + count = Math.pow(2, n), + level; + + if (n === 0) { + g.addNode(1); + } + + for (level = 1; level < count; ++level) { + var root = level, + left = root * 2, + right = root * 2 + 1; + + g.addLink(root, left); + g.addLink(root, right); + } + + return g; +} + +/** + * Creates graph with no links + * + * @param {Number} n of nodes in the graph + */ +function noLinks(n) { + if (n < 0) { + throw new Error("Number of nodes shoul be >= 0"); + } + + var g = createGraph(), i; + for (i = 0; i < n; ++i) { + g.addNode(i); + } + + return g; +} + +},{"ngraph.graph":7}],7:[function(require,module,exports){ +/** + * @fileOverview Contains definition of the core graph object. + */ + + +/** + * @example + * var graph = require('ngraph.graph')(); + * graph.addNode(1); // graph has one node. + * graph.addLink(2, 3); // now graph contains three nodes and one link. + * + */ +module.exports = function () { + // Graph structure is maintained as dictionary of nodes + // and array of links. Each node has 'links' property which + // hold all links related to that node. And general links + // array is used to speed up all links enumeration. This is inefficient + // in terms of memory, but simplifies coding. + + var nodes = {}, + links = [], + // Hash of multi-edges. Used to track ids of edges between same nodes + multiEdges = {}, + nodesCount = 0, + suspendEvents = 0, + + // Accumlates all changes made during graph updates. + // Each change element contains: + // changeType - one of the strings: 'add', 'remove' or 'update'; + // node - if change is related to node this property is set to changed graph's node; + // link - if change is related to link this property is set to changed graph's link; + changes = [], + + fireGraphChanged = function (graph) { + graph.fire('changed', changes); + }, + + // Enter, Exit Mofidication allows bulk graph updates without firing events. + enterModification = function () { + suspendEvents += 1; + }, + + exitModification = function (graph) { + suspendEvents -= 1; + if (suspendEvents === 0 && changes.length > 0) { + fireGraphChanged(graph); + changes.length = 0; + } + }, + + recordNodeChange = function (node, changeType) { + changes.push({node : node, changeType : changeType}); + }, + + recordLinkChange = function (link, changeType) { + changes.push({link : link, changeType : changeType}); + }, + linkConnectionSymbol = '👉 '; + + var graphPart = { + + /** + * Adds node to the graph. If node with given id already exists in the graph + * its data is extended with whatever comes in 'data' argument. + * + * @param nodeId the node's identifier. A string or number is preferred. + * note: Node id should not contain 'linkConnectionSymbol'. This will break link identifiers + * @param [data] additional data for the node being added. If node already + * exists its data object is augmented with the new one. + * + * @return {node} The newly added node or node with given id if it already exists. + */ + addNode : function (nodeId, data) { + if (typeof nodeId === 'undefined') { + throw new Error('Invalid node identifier'); + } + + enterModification(); + + var node = this.getNode(nodeId); + if (!node) { + // TODO: Should I check for linkConnectionSymbol here? + node = new Node(nodeId); + nodesCount++; + + recordNodeChange(node, 'add'); + } else { + recordNodeChange(node, 'update'); + } + + node.data = data; + + nodes[nodeId] = node; + + exitModification(this); + return node; + }, + + /** + * Adds a link to the graph. The function always create a new + * link between two nodes. If one of the nodes does not exists + * a new node is created. + * + * @param fromId link start node id; + * @param toId link end node id; + * @param [data] additional data to be set on the new link; + * + * @return {link} The newly created link + */ + addLink : function (fromId, toId, data) { + enterModification(); + + var fromNode = this.getNode(fromId) || this.addNode(fromId); + var toNode = this.getNode(toId) || this.addNode(toId); + + var linkId = fromId.toString() + linkConnectionSymbol + toId.toString(); + var isMultiEdge = multiEdges.hasOwnProperty(linkId); + if (isMultiEdge || this.hasLink(fromId, toId)) { + if (!isMultiEdge) { + multiEdges[linkId] = 0; + } + linkId += '@' + (++multiEdges[linkId]); + } + + var link = new Link(fromId, toId, data, linkId); + + links.push(link); + + // TODO: this is not cool. On large graphs potentially would consume more memory. + fromNode.links.push(link); + toNode.links.push(link); + + recordLinkChange(link, 'add'); + + exitModification(this); + + return link; + }, + + /** + * Removes link from the graph. If link does not exist does nothing. + * + * @param link - object returned by addLink() or getLinks() methods. + * + * @returns true if link was removed; false otherwise. + */ + removeLink : function (link) { + if (!link) { return false; } + var idx = indexOfElementInArray(link, links); + if (idx < 0) { return false; } + + enterModification(); + + links.splice(idx, 1); + + var fromNode = this.getNode(link.fromId); + var toNode = this.getNode(link.toId); + + if (fromNode) { + idx = indexOfElementInArray(link, fromNode.links); + if (idx >= 0) { + fromNode.links.splice(idx, 1); + } + } + + if (toNode) { + idx = indexOfElementInArray(link, toNode.links); + if (idx >= 0) { + toNode.links.splice(idx, 1); + } + } + + recordLinkChange(link, 'remove'); + + exitModification(this); + + return true; + }, + + /** + * Removes node with given id from the graph. If node does not exist in the graph + * does nothing. + * + * @param nodeId node's identifier passed to addNode() function. + * + * @returns true if node was removed; false otherwise. + */ + removeNode: function (nodeId) { + var node = this.getNode(nodeId); + if (!node) { return false; } + + enterModification(); + + while (node.links.length) { + var link = node.links[0]; + this.removeLink(link); + } + + delete nodes[nodeId]; + nodesCount--; + + recordNodeChange(node, 'remove'); + + exitModification(this); + + return true; + }, + + /** + * Gets node with given identifier. If node does not exist undefined value is returned. + * + * @param nodeId requested node identifier; + * + * @return {node} in with requested identifier or undefined if no such node exists. + */ + getNode : function (nodeId) { + return nodes[nodeId]; + }, + + /** + * Gets number of nodes in this graph. + * + * @return number of nodes in the graph. + */ + getNodesCount : function () { + return nodesCount; + }, + + /** + * Gets total number of links in the graph. + */ + getLinksCount : function () { + return links.length; + }, + + /** + * Gets all links (inbound and outbound) from the node with given id. + * If node with given id is not found null is returned. + * + * @param nodeId requested node identifier. + * + * @return Array of links from and to requested node if such node exists; + * otherwise null is returned. + */ + getLinks : function (nodeId) { + var node = this.getNode(nodeId); + return node ? node.links : null; + }, + + /** + * Invokes callback on each node of the graph. + * + * @param {Function(node)} callback Function to be invoked. The function + * is passed one argument: visited node. + */ + forEachNode : function (callback) { + if (typeof callback !== 'function') { + return; + } + var node; + + for (node in nodes) { + if (nodes.hasOwnProperty(node)) { + if (callback(nodes[node])) { + return; // client doesn't want to proceed. return. + } + } + } + }, + + /** + * Invokes callback on every linked (adjacent) node to the given one. + * + * @param nodeId Identifier of the requested node. + * @param {Function(node, link)} callback Function to be called on all linked nodes. + * The function is passed two parameters: adjacent node and link object itself. + * @param oriented if true graph treated as oriented. + */ + forEachLinkedNode : function (nodeId, callback, oriented) { + var node = this.getNode(nodeId), + i, + link, + linkedNodeId; + + if (node && node.links && typeof callback === 'function') { + // Extraced orientation check out of the loop to increase performance + if (oriented) { + for (i = 0; i < node.links.length; ++i) { + link = node.links[i]; + if (link.fromId === nodeId) { + callback(nodes[link.toId], link); + } + } + } else { + for (i = 0; i < node.links.length; ++i) { + link = node.links[i]; + linkedNodeId = link.fromId === nodeId ? link.toId : link.fromId; + + callback(nodes[linkedNodeId], link); + } + } + } + }, + + /** + * Enumerates all links in the graph + * + * @param {Function(link)} callback Function to be called on all links in the graph. + * The function is passed one parameter: graph's link object. + * + * Link object contains at least the following fields: + * fromId - node id where link starts; + * toId - node id where link ends, + * data - additional data passed to graph.addLink() method. + */ + forEachLink : function (callback) { + var i, length; + if (typeof callback === 'function') { + for (i = 0, length = links.length; i < length; ++i) { + callback(links[i]); + } + } + }, + + /** + * Suspend all notifications about graph changes until + * endUpdate is called. + */ + beginUpdate : function () { + enterModification(); + }, + + /** + * Resumes all notifications about graph changes and fires + * graph 'changed' event in case there are any pending changes. + */ + endUpdate : function () { + exitModification(this); + }, + + /** + * Removes all nodes and links from the graph. + */ + clear : function () { + var that = this; + that.beginUpdate(); + that.forEachNode(function (node) { that.removeNode(node.id); }); + that.endUpdate(); + }, + + /** + * Detects whether there is a link between two nodes. + * Operation complexity is O(n) where n - number of links of a node. + * + * @returns link if there is one. null otherwise. + */ + hasLink : function (fromNodeId, toNodeId) { + // TODO: Use adjacency matrix to speed up this operation. + var node = this.getNode(fromNodeId), + i; + if (!node) { + return null; + } + + for (i = 0; i < node.links.length; ++i) { + var link = node.links[i]; + if (link.fromId === fromNodeId && link.toId === toNodeId) { + return link; + } + } + + return null; // no link. + } + }; + + // Let graph fire events before we return it to the caller. + var eventify = require('ngraph.events'); + eventify(graphPart); + + return graphPart; +}; + +// need this for old browsers. Should this be a separate module? +function indexOfElementInArray(element, array) { + if (array.indexOf) { + return array.indexOf(element); + } + + var len = array.length, + i; + + for (i = 0; i < len; i += 1) { + if (array[i] === element) { + return i; + } + } + + return -1; +} + +/** + * Internal structure to represent node; + */ +function Node(id) { + this.id = id; + this.links = []; + this.data = null; +} + + +/** + * Internal structure to represent links; + */ +function Link(fromId, toId, data, id) { + this.fromId = fromId; + this.toId = toId; + this.data = data; + this.id = id; +} + +},{"ngraph.events":8}],8:[function(require,module,exports){ +module.exports = function(subject) { + validateSubject(subject); + + var eventsStorage = createEventsStorage(subject); + subject.on = eventsStorage.on; + subject.off = eventsStorage.off; + subject.fire = eventsStorage.fire; + return subject; +}; + +function createEventsStorage(subject) { + // Store all event listeners to this hash. Key is event name, value is array + // of callback records. + // + // A callback record consists of callback function and its optional context: + // { 'eventName' => [{callback: function, ctx: object}] } + var registeredEvents = {}; + + return { + on: function (eventName, callback, ctx) { + if (typeof callback !== 'function') { + throw new Error('callback is expected to be a function'); + } + if (!registeredEvents.hasOwnProperty(eventName)) { + registeredEvents[eventName] = []; + } + registeredEvents[eventName].push({callback: callback, ctx: ctx}); + + return subject; + }, + + off: function (eventName, callback) { + var wantToRemoveAll = (typeof eventName === 'undefined'); + if (wantToRemoveAll) { + // Killing old events storage should be enough in this case: + registeredEvents = {}; + return subject; + } + + if (registeredEvents.hasOwnProperty(eventName)) { + var deleteAllCallbacksForEvent = (typeof callback !== 'function'); + if (deleteAllCallbacksForEvent) { + delete registeredEvents[eventName]; + } else { + var callbacks = registeredEvents[eventName]; + for (var i = 0; i < callbacks.length; ++i) { + if (callbacks[i].callback === callback) { + callbacks.splice(i, 1); + } + } + } + } + + return subject; + }, + + fire: function (eventName) { + var noEventsToFire = !registeredEvents.hasOwnProperty(eventName); + if (noEventsToFire) { + return subject; + } + + var callbacks = registeredEvents[eventName]; + var fireArguments = Array.prototype.splice.call(arguments, 1); + for(var i = 0; i < callbacks.length; ++i) { + var callbackInfo = callbacks[i]; + callbackInfo.callback.apply(callbackInfo.ctx, fireArguments); + } + + return subject; + } + }; +} + +function validateSubject(subject) { + if (!subject) { + throw new Error('Eventify cannot use falsy object as events subject'); + } + var reservedWords = ['on', 'fire', 'off']; + for (var i = 0; i < reservedWords.length; ++i) { + if (subject.hasOwnProperty(reservedWords[i])) { + throw new Error("Subject cannot be eventified, since it already has property '" + reservedWords[i] + "'"); + } + } +} + +},{}],9:[function(require,module,exports){ +var NODE_WIDTH = 10; + +module.exports = function (graph, settings) { + var merge = require('ngraph.merge'); + + // Initialize default settings: + settings = merge(settings, { + // What is the background color of a graph? + background: 0x000000, + + // Default physics engine settings + physics: { + springLength: 30, + springCoeff: 0.0008, + dragCoeff: 0.01, + gravity: -1.2, + theta: 1 + } + }); + + // Where do we render our graph? + if (typeof settings.container === 'undefined') { + settings.container = document.body + } + + // If client does not need custom layout algorithm, let's create default one: + var layout = settings.layout; + + if (!layout) { + var createLayout = require('ngraph.forcelayout'), + physics = require('ngraph.physics.simulator'); + + layout = createLayout(graph, physics(settings.physics)); + } + + + var width = settings.container.clientWidth, + height = settings.container.clientHeight; + +// var renderer = PIXI.autoDetectRenderer(width, height, null, false, true); + var renderer = new PIXI.WebGLRenderer(width, height, null, false, true); + + var stage = new PIXI.Stage(settings.background, true); + + stage.interactive = true; + + settings.container.appendChild(renderer.view); + + var graphics = new PIXI.Graphics(); + +// console.log(PIXI.BlurFilter()); + +// var invertFilter = new PIXI.InvertFilter(); + +// var blurFilter1 = new PIXI.BlurFilter(); + +// blurFilter1.blur = 2; + + graphics.position.x = width/2; + graphics.position.y = height/2; + graphics.scale.x = 1; + graphics.scale.y = 1; + +// stage.filters = [blurFilter1] + + stage.addChild(graphics); + + // Default callbacks to build/render nodes + var nodeUIBuilder = defaultCreateNodeUI, + nodeRenderer = defaultNodeRenderer, + linkUIBuilder = defaultCreateLinkUI, + linkRenderer = defaultLinkRenderer; + + // Storage for UI of nodes/links: + var nodeUI = {}, linkUI = {}; + + graph.forEachNode(initNode); + graph.forEachLink(initLink); + + listenToGraphEvents(); + + var pixiGraphics = { + /** + * Allows client to start animation loop, without worrying about RAF stuff. + */ + run: animationLoop, + + /** + * For more sophisticated clients we expose one frame rendering as part of + * API. This may be useful for clients who have their own RAF pipeline. + */ + renderOneFrame: renderOneFrame, + + /** + * This callback creates new UI for a graph node. This becomes helpful + * when you want to precalculate some properties, which otherwise could be + * expensive during rendering frame. + * + * @callback createNodeUICallback + * @param {object} node - graph node for which UI is required. + * @returns {object} arbitrary object which will be later passed to renderNode + */ + /** + * This function allows clients to pass custon node UI creation callback + * + * @param {createNodeUICallback} createNodeUICallback - The callback that + * creates new node UI + * @returns {object} this for chaining. + */ + createNodeUI : function (createNodeUICallback) { + nodeUI = {}; + nodeUIBuilder = createNodeUICallback; + graph.forEachNode(initNode); + return this; + }, + + /** + * This callback is called by pixiGraphics when it wants to render node on + * a screen. + * + * @callback renderNodeCallback + * @param {object} node - result of createNodeUICallback(). It contains anything + * you'd need to render a node + * @param {PIXI.Graphics} ctx - PIXI's rendering context. + */ + /** + * Allows clients to pass custom node rendering callback + * + * @param {renderNodeCallback} renderNodeCallback - Callback which renders + * node. + * + * @returns {object} this for chaining. + */ + renderNode: function (renderNodeCallback) { + nodeRenderer = renderNodeCallback; + return this; + }, + + /** + * This callback creates new UI for a graph link. This becomes helpful + * when you want to precalculate some properties, which otherwise could be + * expensive during rendering frame. + * + * @callback createLinkUICallback + * @param {object} link - graph link for which UI is required. + * @returns {object} arbitrary object which will be later passed to renderNode + */ + /** + * This function allows clients to pass custon node UI creation callback + * + * @param {createLinkUICallback} createLinkUICallback - The callback that + * creates new link UI + * @returns {object} this for chaining. + */ + createLinkUI : function (createLinkUICallback) { + linkUI = {}; + linkUIBuilder = createLinkUICallback; + graph.forEachLink(initLink); + return this; + }, + + /** + * This callback is called by pixiGraphics when it wants to render link on + * a screen. + * + * @callback renderLinkCallback + * @param {object} link - result of createLinkUICallback(). It contains anything + * you'd need to render a link + * @param {PIXI.Graphics} ctx - PIXI's rendering context. + */ + /** + * Allows clients to pass custom link rendering callback + * + * @param {renderLinkCallback} renderLinkCallback - Callback which renders + * link. + * + * @returns {object} this for chaining. + */ + renderLink: function (renderLinkCallback) { + linkRenderer = renderLinkCallback; + return this; + }, + + /** + * Tries to get node at (x, y) graph coordinates. By default renderer assumes + * width and height of the node is 10 pixels. But if your createNodeUICallback + * returns object with `width` and `height` attributes, they are considered + * as actual dimensions of a node + * + * @param {Number} x - x coordinate of a node in layout's coordinates + * @param {Number} y - y coordinate of a node in layout's coordinates + * @returns {Object} - acutal graph node located at (x, y) coordinates. + * If there is no node in that are `undefined` is returned. + * + * TODO: This should be part of layout itself + */ + getNodeAt: getNodeAt, + + /** + * [Read only] Current layout algorithm. If you want to pass custom layout + * algorithm, do it via `settings` argument of ngraph.pixi. + */ + layout: layout, + + // TODO: These properties seem to only be required fo graph input. I'd really + // like to hide them, but not sure how to do it nicely + domContainer: renderer.view, + graphGraphics: graphics, + stage: stage + }; + + // listen to mouse events + var graphInput = require('./lib/graphInput'); + graphInput(pixiGraphics, layout); + + return pixiGraphics; + +/////////////////////////////////////////////////////////////////////////////// +// Public API is over +/////////////////////////////////////////////////////////////////////////////// + + function animationLoop() { + layout.step(); + renderOneFrame(); + requestAnimFrame(animationLoop); + } + + function renderOneFrame() { + graphics.clear(); + + Object.keys(linkUI).forEach(renderLink); + Object.keys(nodeUI).forEach(renderNode); + + renderer.render(stage); + } + + function renderLink(linkId) { + linkRenderer(linkUI[linkId], graphics); + } + + function renderNode(nodeId) { + nodeRenderer(nodeUI[nodeId], graphics); + } + + function initNode(node) { + var ui = nodeUIBuilder(node); + // augment it with position data: + ui.pos = layout.getNodePosition(node.id); + // and store for subsequent use: + nodeUI[node.id] = ui; + } + + function initLink(link) { + var ui = linkUIBuilder(link); + ui.from = layout.getNodePosition(link.fromId); + ui.to = layout.getNodePosition(link.toId); + linkUI[link.id] = ui; + } + + function defaultCreateNodeUI(node) { + return {}; + } + + function defaultCreateLinkUI(link) { + return {}; + } + + function defaultNodeRenderer(node) { + var x = node.pos.x - NODE_WIDTH/2, + y = node.pos.y - NODE_WIDTH/2; + + graphics.beginFill(0xFF3300); + graphics.drawRect(x, y, NODE_WIDTH, NODE_WIDTH); + } + + function defaultLinkRenderer(link) { + graphics.lineStyle(1, 0xcccccc, 1); + graphics.moveTo(link.from.x, link.from.y); + graphics.lineTo(link.to.x, link.to.y); + } + + function getNodeAt(x, y) { + var half = NODE_WIDTH/2; + // currently it's a linear search, but nothing stops us from refactoring + // this into spatial lookup data structure in future: + for (var nodeId in nodeUI) { + if (nodeUI.hasOwnProperty(nodeId)) { + var node = nodeUI[nodeId]; + var pos = node.pos; + var width = node.width || NODE_WIDTH; + var half = width/2; + var insideNode = pos.x - half < x && x < pos.x + half && + pos.y - half < y && y < pos.y + half; + + if (insideNode) { + return graph.getNode(nodeId); + } + } + } + } + + function listenToGraphEvents() { + graph.on('changed', onGraphChanged); + } + + function onGraphChanged(changes) { + for (var i = 0; i < changes.length; ++i) { + var change = changes[i]; + if (change.changeType === 'add') { + if (change.node) { + initNode(change.node); + } + if (change.link) { + initLink(change.link); + } + } else if (change.changeType === 'remove') { + if (change.node) { + delete nodeUI[change.node.id]; + } + if (change.link) { + delete linkUI[change.link.id]; + } + } + } + } +} + +},{"./lib/graphInput":11,"ngraph.forcelayout":12,"ngraph.merge":15,"ngraph.physics.simulator":16}],10:[function(require,module,exports){ +/** + * This module unifies handling of mouse whee event accross different browsers + * + * See https://developer.mozilla.org/en-US/docs/Web/Reference/Events/wheel?redirectlocale=en-US&redirectslug=DOM%2FMozilla_event_reference%2Fwheel + * for more details + */ +module.exports = addWheelListener; + +var prefix = "", _addEventListener, onwheel, support; + +// detect event model +if ( window.addEventListener ) { + _addEventListener = "addEventListener"; +} else { + _addEventListener = "attachEvent"; + prefix = "on"; +} + +// detect available wheel event +support = "onwheel" in document.createElement("div") ? "wheel" : // Modern browsers support "wheel" + document.onmousewheel !== undefined ? "mousewheel" : // Webkit and IE support at least "mousewheel" + "DOMMouseScroll"; // let's assume that remaining browsers are older Firefox + +function addWheelListener( elem, callback, useCapture ) { + _addWheelListener( elem, support, callback, useCapture ); + + // handle MozMousePixelScroll in older Firefox + if( support == "DOMMouseScroll" ) { + _addWheelListener( elem, "MozMousePixelScroll", callback, useCapture ); + } +}; + +function _addWheelListener( elem, eventName, callback, useCapture ) { + elem[ _addEventListener ]( prefix + eventName, support == "wheel" ? callback : function( originalEvent ) { + !originalEvent && ( originalEvent = window.event ); + + // create a normalized event object + var event = { + // keep a ref to the original event object + originalEvent: originalEvent, + target: originalEvent.target || originalEvent.srcElement, + type: "wheel", + deltaMode: originalEvent.type == "MozMousePixelScroll" ? 0 : 1, + deltaX: 0, + delatZ: 0, + preventDefault: function() { + originalEvent.preventDefault ? + originalEvent.preventDefault() : + originalEvent.returnValue = false; + } + }; + + // calculate deltaY (and deltaX) according to the event + if ( support == "mousewheel" ) { + event.deltaY = - 1/40 * originalEvent.wheelDelta; + // Webkit also support wheelDeltaX + originalEvent.wheelDeltaX && ( event.deltaX = - 1/40 * originalEvent.wheelDeltaX ); + } else { + event.deltaY = originalEvent.detail; + } + + // it's time to fire the callback + return callback( event ); + + }, useCapture || false ); +} + +},{}],11:[function(require,module,exports){ +/** + * Tracks mouse input and updates pixi graphics (zoom/pan). + * + * Note: I don't really like how this module is tightly coupled with graphics. + * If you have dieas how to make this copuling loose, please let me know! + */ +module.exports = function (graphics, layout) { + var addWheelListener = require('./addWheelListener'); + var graphGraphics = graphics.graphGraphics; + + addWheelListener(graphics.domContainer, function (e) { + zoom(e.clientX, e.clientY, e.deltaY < 0); + }); + + addDragListener(); + + var getGraphCoordinates = (function () { + var ctx = { + global: { x: 0, y: 0} // store it inside closure to avoid GC pressure + }; + + return function (x, y) { + ctx.global.x = x; ctx.global.y = y; + return PIXI.InteractionData.prototype.getLocalPosition.call(ctx, graphGraphics); + } + }()); + + function zoom(x, y, isZoomIn) { + direction = isZoomIn ? 1 : -1; + var factor = (1 + direction * 0.1); + graphGraphics.scale.x *= factor; + graphGraphics.scale.y *= factor; + + // Technically code below is not required, but helps to zoom on mouse + // cursor, instead center of graphGraphics coordinates + var beforeTransform = getGraphCoordinates(x, y); + graphGraphics.updateTransform(); + var afterTransform = getGraphCoordinates(x, y); + + graphGraphics.position.x += (afterTransform.x - beforeTransform.x) * graphGraphics.scale.x; + graphGraphics.position.y += (afterTransform.y - beforeTransform.y) * graphGraphics.scale.y; + graphGraphics.updateTransform(); + } + + function addDragListener() { + var stage = graphics.stage; + stage.setInteractive(true); + + var isDragging = false, + nodeUnderCursor, + prevX, prevY; + + stage.mousedown = function (moveData) { + var pos = moveData.global; + var graphPos = getGraphCoordinates(pos.x, pos.y); + nodeUnderCursor = graphics.getNodeAt(graphPos.x, graphPos.y); + if (nodeUnderCursor) { + // just to make sure layouter will not attempt to move this node + // based on physical forces. Now it's completely under our control: + layout.pinNode(nodeUnderCursor, true); + } + + prevX = pos.x; prevY = pos.y; + isDragging = true; + }; + + stage.mousemove = function (moveData) { + if (!isDragging) { + return; + } + var pos = moveData.global; + + if (nodeUnderCursor) { + var graphPos = getGraphCoordinates(pos.x, pos.y); + layout.setNodePosition(nodeUnderCursor.id, graphPos.x, graphPos.y); + } else { + var dx = pos.x - prevX; + var dy = pos.y - prevY; + prevX = pos.x; prevY = pos.y; + graphGraphics.position.x += dx; + graphGraphics.position.y += dy; + } + }; + + stage.mouseup = function (moveDate) { + isDragging = false; + if (nodeUnderCursor) { + draggingNode = null; + layout.pinNode(nodeUnderCursor, false); + } + }; + } +} + +},{"./addWheelListener":10}],12:[function(require,module,exports){ +module.exports = createLayout; + +// Maximum movement of the system at which system should be considered as stable +var MAX_MOVEMENT = 0.001; + +/** + * Creates force based layout for a given graph. + * @param {ngraph.graph} graph which needs to be layed out + * @param {ngraph.physics.simulator=} physicsSimulator if you need custom settings + * for physics simulator you can pass your own simulator here. If it's not passed + * a default one will be created + */ +function createLayout(graph, physicsSimulator) { + if (!graph) { + throw new Error('Graph structure cannot be undefined'); + } + + var random = require('ngraph.random').random(42), + simulator = require('ngraph.physics.simulator'), + physics = require('ngraph.physics.primitives'); + + physicsSimulator = physicsSimulator || simulator(); + + var nodeBodies = {}, + springs = {}, + graphRect = { x1: 0, y1: 0, x2: 0, y2: 0 }; + + // Initialize physical objects according to what we have in the graph: + initPhysics(); + listenToGraphEvents(); + + return { + /** + * Performs one step of iterative layout algorithm + */ + step: function() { + var totalMovement = physicsSimulator.step(); + updateGraphRect(); + + return totalMovement < MAX_MOVEMENT; + }, + + /** + * For a given `nodeId` returns position + */ + getNodePosition: function (nodeId) { + return getInitializedBody(nodeId).pos; + }, + + /** + * Sets position of a node to a given coordinates + */ + setNodePosition: function (nodeId, x, y) { + var body = getInitializedBody(nodeId); + body.prevPos.x = body.pos.x = x; + body.prevPos.y = body.pos.y = y; + }, + /** + * @returns {Object} Link position by link id + * @returns {Object.from} {x, y} coordinates of link start + * @returns {Object.to} {x, y} coordinates of link end + */ + getLinkPosition: function (linkId) { + var spring = springs[linkId]; + if (spring) { + return { + from: spring.from.pos, + to: spring.to.pos + }; + } + }, + + /** + * @returns {Object} area required to fit in the graph. Object contains + * `x1`, `y1` - top left coordinates + * `x2`, `y2` - bottom right coordinates + */ + getGraphRect: function () { + return graphRect; + }, + + /* + * Requests layout algorithm to pin/unpin node to its current position + * Pinned nodes should not be affected by layout algorithm and always + * remain at their position + */ + pinNode: function (node, isPinned) { + var body = getInitializedBody(node.id); + body.isPinned = !!isPinned; + }, + + /** + * Checks whether given graph's node is currently pinned + */ + isNodePinned: function (node) { + return getInitializedBody(node.id).isPinned; + }, + + /** + * Request to release all resources + */ + dispose: function() { + graph.off('changed', onGraphChanged); + }, + + /** + * [Read only] Gets current physics simulator + */ + simulator: physicsSimulator + }; + + function listenToGraphEvents() { + graph.on('changed', onGraphChanged); + } + + function onGraphChanged(changes) { + for (var i = 0; i < changes.length; ++i) { + var change = changes[i]; + if (change.changeType === 'add') { + if (change.node) { + initBody(change.node.id); + } + if (change.link) { + initLink(change.link); + } + } else if (change.changeType === 'remove') { + if (change.node) { + releaseNode(change.node); + } + if (change.link) { + releaseLink(change.link); + } + } + } + } + + function initPhysics() { + graph.forEachNode(function (node) { + initBody(node.id); + }); + graph.forEachLink(initLink); + } + + function initBody(nodeId) { + var body = nodeBodies[nodeId]; + if (!body) { + var node = graph.getNode(nodeId); + if (!node) { + throw new Error('initBody() was called with unknown node id'); + } + + var pos = getBestInitialNodePosition(node); + body = new physics.Body(pos.x, pos.y); + // we need to augment body with previous position to let users pin them + body.prevPos = new physics.Vector2d(pos.x, pos.y); + + nodeBodies[nodeId] = body; + updateBodyMass(nodeId); + + if (isNodeOriginallyPinned(node)) { + body.isPinned = true; + } + + physicsSimulator.addBody(body); + } + } + + function releaseNode(node) { + var nodeId = node.id; + var body = nodeBodies[nodeId]; + if (body) { + nodeBodies[nodeId] = null; + delete nodeBodies[nodeId]; + + physicsSimulator.removeBody(body); + if (graph.getNodesCount() === 0) { + graphRect.x1 = graphRect.y1 = 0; + graphRect.x2 = graphRect.y2 = 0; + } + } + } + + function initLink(link) { + updateBodyMass(link.fromId); + updateBodyMass(link.toId); + + var fromBody = nodeBodies[link.fromId], + toBody = nodeBodies[link.toId], + spring = physicsSimulator.addSpring(fromBody, toBody, link.length); + + springs[link.id] = spring; + } + + function releaseLink(link) { + var spring = springs[link.id]; + if (spring) { + var from = graph.getNode(link.fromId), + to = graph.getNode(link.toId); + + if (from) updateBodyMass(from.id); + if (to) updateBodyMass(to.id); + + delete springs[link.id]; + + physicsSimulator.removeSpring(spring); + } + } + + function getBestInitialNodePosition(node) { + // TODO: Initial position could be picked better, e.g. take into + // account all neighbouring nodes/links, not only one. + // How about center of mass? + if (node.position) { + return node.position; + } + + var baseX = (graphRect.x1 + graphRect.x2) / 2, + baseY = (graphRect.y1 + graphRect.y2) / 2, + springLength = physicsSimulator.springLength(); + + if (node.links && node.links.length > 0) { + var firstLink = node.links[0], + otherBody = firstLink.fromId !== node.id ? nodeBodies[firstLink.fromId] : nodeBodies[firstLink.toId]; + if (otherBody && otherBody.pos) { + baseX = otherBody.pos.x; + baseY = otherBody.pos.y; + } + } + + return { + x: baseX + random.next(springLength) - springLength / 2, + y: baseY + random.next(springLength) - springLength / 2 + }; + } + + function updateBodyMass(nodeId) { + var body = nodeBodies[nodeId]; + body.mass = nodeMass(nodeId); + } + + + function updateGraphRect() { + if (graph.getNodesCount() === 0) { + // don't have to wory here. + return; + } + + var x1 = Number.MAX_VALUE, + y1 = Number.MAX_VALUE, + x2 = Number.MIN_VALUE, + y2 = Number.MIN_VALUE; + + // this is O(n), could it be done faster with quadtree? + for (var key in nodeBodies) { + if (nodeBodies.hasOwnProperty(key)) { + // how about pinned nodes? + var body = nodeBodies[key]; + if (isBodyPinned(body)) { + body.pos.x = body.prevPos.x; + body.pos.y = body.prevPos.y; + } else { + body.prevPos.x = body.pos.x; + body.prevPos.y = body.pos.y; + } + if (body.pos.x < x1) { + x1 = body.pos.x; + } + if (body.pos.x > x2) { + x2 = body.pos.x; + } + if (body.pos.y < y1) { + y1 = body.pos.y; + } + if (body.pos.y > y2) { + y2 = body.pos.y; + } + } + } + + graphRect.x1 = x1; + graphRect.x2 = x2; + graphRect.y1 = y1; + graphRect.y2 = y2; + } + + /** + * Checks whether graph node has in its settings pinned attribute, + * which means layout algorithm cannot move it. Node can be preconfigured + * as pinned, if it has "isPinned" attribute, or when node.data has it. + * + * @param {Object} node a graph node to check + * @return {Boolean} true if node should be treated as pinned; false otherwise. + */ + function isNodeOriginallyPinned(node) { + return (node && (node.isPinned || (node.data && node.data.isPinned))); + } + + /** + * Checks whether given physical body should be treated as pinned. Unlinke + * `isNodeOriginallyPinned` this operates on body object, which is specific to layout + * instance. Thus two layouters can independntly pin bodies, which represent + * same node of a source graph. + * + * @param {ngraph.physics.Body} body - body to check + * @return {Boolean} true if body should be treated as pinned; false otherwise. + */ + function isBodyPinned (body) { + return body.isPinned; + } + + function getInitializedBody(nodeId) { + var body = nodeBodies[nodeId]; + if (!body) { + initBody(nodeId); + body = nodeBodies[nodeId]; + } + return body; + } + + /** + * Calculates mass of a body, which corresponds to node with given id. + * + * @param {String|Number} nodeId identifier of a node, for which body mass needs to be calculated + * @returns {Number} recommended mass of the body; + */ + function nodeMass(nodeId) { + return 1 + graph.getLinks(nodeId).length / 3.0; + } +} + +},{"ngraph.physics.primitives":13,"ngraph.physics.simulator":16,"ngraph.random":14}],13:[function(require,module,exports){ +module.exports = { + Body: Body, + Vector2d: Vector2d + // that's it for now +}; + +function Body(x, y) { + this.pos = new Vector2d(x, y); + this.force = new Vector2d(); + this.velocity = new Vector2d(); + this.mass = 1; +} + +function Vector2d(x, y) { + this.x = typeof x === 'number' ? x : 0; + this.y = typeof y === 'number' ? y : 0; +} + +},{}],14:[function(require,module,exports){ +module.exports = { + random: random, + randomIterator: randomIterator +}; + +/** + * Creates seeded PRNG with two methods: + * next() and nextDouble() + */ +function random(inputSeed) { + var seed = typeof inputSeed === 'number' ? inputSeed : (+ new Date()); + var randomFunc = function() { + // Robert Jenkins' 32 bit integer hash function. + seed = ((seed + 0x7ed55d16) + (seed << 12)) & 0xffffffff; + seed = ((seed ^ 0xc761c23c) ^ (seed >>> 19)) & 0xffffffff; + seed = ((seed + 0x165667b1) + (seed << 5)) & 0xffffffff; + seed = ((seed + 0xd3a2646c) ^ (seed << 9)) & 0xffffffff; + seed = ((seed + 0xfd7046c5) + (seed << 3)) & 0xffffffff; + seed = ((seed ^ 0xb55a4f09) ^ (seed >>> 16)) & 0xffffffff; + return (seed & 0xfffffff) / 0x10000000; + }; + + return { + /** + * Generates random integer number in the range from 0 (inclusive) to maxValue (exclusive) + * + * @param maxValue Number REQUIRED. Ommitting this number will result in NaN values from PRNG. + */ + next : function (maxValue) { + return Math.floor(randomFunc() * maxValue); + }, + + /** + * Generates random double number in the range from 0 (inclusive) to 1 (exclusive) + * This function is the same as Math.random() (except that it could be seeded) + */ + nextDouble : function () { + return randomFunc(); + } + }; +} + +/* + * Creates iterator over array, which returns items of array in random order + * Time complexity is guaranteed to be O(n); + */ +function randomIterator(array, customRandom) { + var localRandom = customRandom || random(); + if (typeof localRandom.next !== 'function') { + throw new Error('customRandom does not match expected API: next() function is missing'); + } + + return { + forEach : function (callback) { + var i, j, t; + for (i = array.length - 1; i > 0; --i) { + j = localRandom.next(i + 1); // i inclusive + t = array[j]; + array[j] = array[i]; + array[i] = t; + + callback(t); + } + + if (array.length) { + callback(array[0]); + } + }, + + /** + * Shuffles array randomly, in place. + */ + shuffle : function () { + var i, j, t; + for (i = array.length - 1; i > 0; --i) { + j = localRandom.next(i + 1); // i inclusive + t = array[j]; + array[j] = array[i]; + array[i] = t; + } + + return array; + } + }; +} + +},{}],15:[function(require,module,exports){ +module.exports = merge; + +/** + * Augments `target` with properties in `options`. Does not override + * target's properties if they are defined and matches expected type in + * options + * + * @returns {Object} merged object + */ +function merge(target, options) { + var key; + if (!target) { target = {}; } + if (options) { + for (key in options) { + if (options.hasOwnProperty(key)) { + var targetHasIt = target.hasOwnProperty(key), + optionsValueType = typeof options[key], + shouldReplace = !targetHasIt || (typeof target[key] !== optionsValueType); + + if (shouldReplace) { + target[key] = options[key]; + } else if (optionsValueType === 'object') { + // go deep, don't care about loops here, we are simple API!: + target[key] = merge(target[key], options[key]); + } + } + } + } + + return target; +} + +},{}],16:[function(require,module,exports){ +/** + * Manages a simulation of physical forces acting on bodies and springs. + */ +module.exports = physicsSimulator; + +function physicsSimulator(settings) { + var Spring = require('./lib/spring'); + var createQuadTree = require('ngraph.quadtreebh'); + var createDragForce = require('./lib/dragForce'); + var createSpringForce = require('./lib/springForce'); + var integrate = require('./lib/eulerIntegrator'); + var expose = require('./lib/exposeProperties'); + var merge = require('ngraph.merge'); + + settings = merge(settings, { + /** + * Ideal length for links (springs in physical model). + */ + springLength: 30, + + /** + * Hook's law coefficient. 1 - solid spring. + */ + springCoeff: 0.0008, + + /** + * Coulomb's law coefficient. It's used to repel nodes thus should be negative + * if you make it positive nodes start attract each other :). + */ + gravity: -1.2, + + /** + * Theta coeffiecient from Barnes Hut simulation. Ranged between (0, 1). + * The closer it's to 1 the more nodes algorithm will have to go through. + * Setting it to one makes Barnes Hut simulation no different from + * brute-force forces calculation (each node is considered). + */ + theta: 0.8, + + /** + * Drag force coefficient. Used to slow down system, thus should be less than 1. + * The closer it is to 0 the less tight system will be. + */ + dragCoeff: 0.02, + + /** + * Default time step (dt) for forces integration + */ + timeStep : 20 + }); + + var bodies = [], // Bodies in this simulation. + springs = [], // Springs in this simulation. + quadTree = createQuadTree(settings), + springForce = createSpringForce(settings), + dragForce = createDragForce(settings); + + var publicApi = { + /** + * Array of bodies, registered with current simulator + * + * Note: To add new body, use addBody() method. This property is only + * exposed for testing/performance purposes. + */ + bodies: bodies, + + /** + * Performs one step of force simulation. + * + * @returns {Number} Total movement of the system. Calculated as: + * (total distance traveled by bodies)^2/(total # of bodies) + */ + step: function () { + // I'm reluctant to check timeStep here, since this method is going to be + // super hot, I don't want to add more complexity to it + accumulateForces(); + return integrate(bodies, settings.timeStep); + }, + + /** + * Adds body to the system + * + * @param {ngraph.physics.primitives.Body} body physical body + * + * @returns {ngraph.physics.primitives.Body} added body + */ + addBody: function (body) { + if (!body) { + throw new Error('Body is required'); + } + bodies.push(body); + + return body; + }, + + /** + * Removes body from the system + * + * @param {ngraph.physics.primitives.Body} body to remove + * + * @returns {Boolean} true if body found and removed. falsy otherwise; + */ + removeBody: function (body) { + if (!body) { return; } + var idx = bodies.indexOf(body); + if (idx > -1) { + bodies.splice(idx, 1); + return true; + } + }, + + /** + * Adds a spring to this simulation. + * + * @returns {Object} - a handle for a spring. If you want to later remove + * spring pass it to removeSpring() method. + */ + addSpring: function (body1, body2, springLength, springWeight, springCoefficient) { + if (!body1 || !body2) { + throw new Error('Cannot add null spring to force simulator'); + } + + if (typeof springLength !== 'number') { + springLength = -1; // assume global configuration + } + + var spring = new Spring(body1, body2, springLength, springCoefficient >= 0 ? springCoefficient : -1, springWeight); + springs.push(spring); + + // TODO: could mark simulator as dirty. + return spring; + }, + + /** + * Removes spring from the system + * + * @param {Object} spring to remove. Spring is an object returned by addSpring + * + * @returns {Boolean} true if spring found and removed. falsy otherwise; + */ + removeSpring: function (spring) { + if (!spring) { return; } + var idx = springs.indexOf(spring); + if (idx > -1) { + springs.splice(idx, 1); + return true; + } + }, + + gravity: function (value) { + if (value !== undefined) { + settings.gravity = value; + quadTree.options({gravity: value}); + return this; + } else { + return settings.gravity; + } + }, + + theta: function (value) { + if (value !== undefined) { + settings.theta = value; + quadTree.options({theta: value}); + return this; + } else { + return settings.theta; + } + } + } + + // allow settings modification via public API: + expose(settings, publicApi); + + return publicApi; + + function accumulateForces() { + // Accumulate forces acting on bodies. + var body, + i = bodies.length; + + if (i) { + // only add bodies if there the array is not empty: + quadTree.insertBodies(bodies); // performance: O(n * log n) + while (i--) { + body = bodies[i]; + body.force.x = 0; + body.force.y = 0; + + quadTree.updateBodyForce(body); + dragForce.update(body); + } + } + + i = springs.length; + while(i--) { + springForce.update(springs[i]); + } + } +}; + +},{"./lib/dragForce":17,"./lib/eulerIntegrator":18,"./lib/exposeProperties":19,"./lib/spring":20,"./lib/springForce":21,"ngraph.merge":15,"ngraph.quadtreebh":22}],17:[function(require,module,exports){ +/** + * Represents drag force, which reduces force value on each step by given + * coefficient. + * + * @param {Object} options for the drag force + * @param {Number=} options.dragCoeff drag force coefficient. 0.1 by default + */ +module.exports = function (options) { + var merge = require('ngraph.merge'), + expose = require('./exposeProperties'); + + options = merge(options, { + dragCoeff: 0.02 + }); + + var api = { + update : function (body) { + body.force.x -= options.dragCoeff * body.velocity.x; + body.force.y -= options.dragCoeff * body.velocity.y; + } + }; + + // let easy access to dragCoeff: + expose(options, api, ['dragCoeff']); + + return api; +}; + +},{"./exposeProperties":19,"ngraph.merge":15}],18:[function(require,module,exports){ +/** + * Performs forces integration, using given timestep. Uses Euler method to solve + * differential equation (http://en.wikipedia.org/wiki/Euler_method ). + * + * @returns {Number} squared distance of total position updates. + */ + +module.exports = integrate; + +function integrate(bodies, timeStep) { + var dx = 0, tx = 0, + dy = 0, ty = 0, + i, + max = bodies.length; + + for (i = 0; i < max; ++i) { + var body = bodies[i], + coeff = timeStep / body.mass; + + body.velocity.x += coeff * body.force.x; + body.velocity.y += coeff * body.force.y; + var vx = body.velocity.x, + vy = body.velocity.y, + v = Math.sqrt(vx * vx + vy * vy); + + if (v > 1) { + body.velocity.x = vx / v; + body.velocity.y = vy / v; + } + + dx = timeStep * body.velocity.x; + dy = timeStep * body.velocity.y; + + body.pos.x += dx; + body.pos.y += dy; + + // TODO: this is not accurate. Total value should be absolute + tx += dx; ty += dy; + } + + return (tx * tx + ty * ty)/bodies.length; +} + +},{}],19:[function(require,module,exports){ +module.exports = exposeProperties; + +/** + * Augments `target` object with getter/setter functions, which modify settings + * + * @example + * var target = {}; + * exposeProperties({ age: 42}, target); + * target.age(); // returns 42 + * target.age(24); // make age 24; + * + * var filteredTarget = {}; + * exposeProperties({ age: 42, name: 'John'}, filteredTarget, ['name']); + * filteredTarget.name(); // returns 'John' + * filteredTarget.age === undefined; // true + */ +function exposeProperties(settings, target, filter) { + var needsFilter = Object.prototype.toString.call(filter) === '[object Array]'; + if (needsFilter) { + for (var i = 0; i < filter.length; ++i) { + augment(settings, target, filter[i]); + } + } else { + for (var key in settings) { + augment(settings, target, key); + } + } +} + +function augment(source, target, key) { + if (source.hasOwnProperty(key)) { + if (typeof target[key] === 'function') { + // this accessor is already defined. Ignore it + return; + } + target[key] = function (value) { + if (value !== undefined) { + source[key] = value; + return target; + } + return source[key]; + } + } +} + +},{}],20:[function(require,module,exports){ +module.exports = Spring; + +/** + * Represents a physical spring. Spring connects two bodies, has rest length + * stiffness coefficient and optional weight + */ +function Spring(fromBody, toBody, length, coeff, weight) { + this.from = fromBody; + this.to = toBody; + this.length = length; + this.coeff = coeff; + + this.weight = typeof weight === 'number' ? weight : 1; +}; + +},{}],21:[function(require,module,exports){ +/** + * Represents spring force, which updates forces acting on two bodies, conntected + * by a spring. + * + * @param {Object} options for the spring force + * @param {Number=} options.springCoeff spring force coefficient. + * @param {Number=} options.springLength desired length of a spring at rest. + */ +module.exports = function (options) { + var merge = require('ngraph.merge'); + var random = require('ngraph.random').random(42); + var expose = require('./exposeProperties'); + + options = merge(options, { + springCoeff: 0.0002, + springLength: 80 + }); + + var api = { + /** + * Upsates forces acting on a spring + */ + update : function (spring) { + var body1 = spring.from, + body2 = spring.to, + length = spring.length < 0 ? options.springLength : spring.length, + dx = body2.pos.x - body1.pos.x, + dy = body2.pos.y - body1.pos.y, + r = Math.sqrt(dx * dx + dy * dy); + + if (r === 0) { + dx = (random.nextDouble() - 0.5) / 50; + dy = (random.nextDouble() - 0.5) / 50; + r = Math.sqrt(dx * dx + dy * dy); + } + + var d = r - length; + var coeff = ((!spring.coeff || spring.coeff < 0) ? options.springCoeff : spring.coeff) * d / r * spring.weight; + + body1.force.x += coeff * dx; + body1.force.y += coeff * dy; + + body2.force.x -= coeff * dx; + body2.force.y -= coeff * dy; + } + }; + + expose(options, api, ['springCoeff', 'springLength']); + return api; +} + +},{"./exposeProperties":19,"ngraph.merge":15,"ngraph.random":26}],22:[function(require,module,exports){ +/** + * This is Barnes Hut simulation algorithm. Implementation + * is adopted to non-recursive solution, since certain browsers + * handle recursion extremly bad. + * + * http://www.cs.princeton.edu/courses/archive/fall03/cs126/assignments/barnes-hut.html + */ + +module.exports = function (options) { + options = options || {}; + options.gravity = typeof options.gravity === 'number' ? options.gravity : -1; + options.theta = typeof options.theta === 'number' ? options.theta : 0.8; + + // we require deterministic randomness here + var random = require('ngraph.random').random(1984), + Node = require('./node'), + InsertStack = require('./insertStack'), + isSamePosition = require('./isSamePosition'); + + var gravity = options.gravity, + updateQueue = [], + insertStack = new InsertStack(), + theta = options.theta, + + nodesCache = [], + currentInCache = 0, + newNode = function () { + // To avoid pressure on GC we reuse nodes. + var node = nodesCache[currentInCache]; + if (node) { + node.quads[0] = null; + node.quads[1] = null; + node.quads[2] = null; + node.quads[3] = null; + node.body = null; + node.mass = node.massX = node.massY = 0; + node.left = node.right = node.top = node.bottom = 0; + } else { + node = new Node(); + nodesCache[currentInCache] = node; + } + + ++currentInCache; + return node; + }, + + root = newNode(), + + // Inserts body to the tree + insert = function (newBody) { + insertStack.reset(); + insertStack.push(root, newBody); + + while (!insertStack.isEmpty()) { + var stackItem = insertStack.pop(), + node = stackItem.node, + body = stackItem.body; + + if (!node.body) { + // This is internal node. Update the total mass of the node and center-of-mass. + var x = body.pos.x; + var y = body.pos.y; + node.mass = node.mass + body.mass; + node.massX = node.massX + body.mass * x; + node.massY = node.massY + body.mass * y; + + // Recursively insert the body in the appropriate quadrant. + // But first find the appropriate quadrant. + var quadIdx = 0, // Assume we are in the 0's quad. + left = node.left, + right = (node.right + left) / 2, + top = node.top, + bottom = (node.bottom + top) / 2; + + if (x > right) { // somewhere in the eastern part. + quadIdx = quadIdx + 1; + var oldLeft = left; + left = right; + right = right + (right - oldLeft); + } + if (y > bottom) { // and in south. + quadIdx = quadIdx + 2; + var oldTop = top; + top = bottom; + bottom = bottom + (bottom - oldTop); + } + + var child = node.quads[quadIdx]; + if (!child) { + // The node is internal but this quadrant is not taken. Add + // subnode to it. + child = newNode(); + child.left = left; + child.top = top; + child.right = right; + child.bottom = bottom; + child.body = body; + + node.quads[quadIdx] = child; + } else { + // continue searching in this quadrant. + insertStack.push(child, body); + } + } else { + // We are trying to add to the leaf node. + // We have to convert current leaf into internal node + // and continue adding two nodes. + var oldBody = node.body; + node.body = null; // internal nodes do not cary bodies + + if (isSamePosition(oldBody.pos, body.pos)) { + // Prevent infinite subdivision by bumping one node + // anywhere in this quadrant + if (node.right - node.left < 1e-8) { + // This is very bad, we ran out of precision. + // if we do not return from the method we'll get into + // infinite loop here. So we sacrifice correctness of layout, and keep the app running + // Next layout iteration should get larger bounding box in the first step and fix this + return; + } + do { + var offset = random.nextDouble(); + var dx = (node.right - node.left) * offset; + var dy = (node.bottom - node.top) * offset; + + oldBody.pos.x = node.left + dx; + oldBody.pos.y = node.top + dy; + // Make sure we don't bump it out of the box. If we do, next iteration should fix it + } while (isSamePosition(oldBody.pos, body.pos)); + + } + // Next iteration should subdivide node further. + insertStack.push(node, oldBody); + insertStack.push(node, body); + } + } + }, + + update = function (sourceBody) { + var queue = updateQueue, + v, + dx, + dy, + r, + queueLength = 1, + shiftIdx = 0, + pushIdx = 1; + + queue[0] = root; + + while (queueLength) { + var node = queue[shiftIdx], + body = node.body; + + queueLength -= 1; + shiftIdx += 1; + // technically there should be external "if (body !== sourceBody) {" + // but in practice it gives slightghly worse performance, and does not + // have impact on layout correctness + if (body && body !== sourceBody) { + // If the current node is a leaf node (and it is not source body), + // calculate the force exerted by the current node on body, and add this + // amount to body's net force. + dx = body.pos.x - sourceBody.pos.x; + dy = body.pos.y - sourceBody.pos.y; + r = Math.sqrt(dx * dx + dy * dy); + + if (r === 0) { + // Poor man's protection against zero distance. + dx = (random.nextDouble() - 0.5) / 50; + dy = (random.nextDouble() - 0.5) / 50; + r = Math.sqrt(dx * dx + dy * dy); + } + + // This is standard gravition force calculation but we divide + // by r^3 to save two operations when normalizing force vector. + v = gravity * body.mass * sourceBody.mass / (r * r * r); + sourceBody.force.x += v * dx; + sourceBody.force.y += v * dy; + } else { + // Otherwise, calculate the ratio s / r, where s is the width of the region + // represented by the internal node, and r is the distance between the body + // and the node's center-of-mass + dx = node.massX / node.mass - sourceBody.pos.x; + dy = node.massY / node.mass - sourceBody.pos.y; + r = Math.sqrt(dx * dx + dy * dy); + + if (r === 0) { + // Sorry about code duplucation. I don't want to create many functions + // right away. Just want to see performance first. + dx = (random.nextDouble() - 0.5) / 50; + dy = (random.nextDouble() - 0.5) / 50; + r = Math.sqrt(dx * dx + dy * dy); + } + // If s / r < θ, treat this internal node as a single body, and calculate the + // force it exerts on body b, and add this amount to b's net force. + if ((node.right - node.left) / r < theta) { + // in the if statement above we consider node's width only + // because the region was squarified during tree creation. + // Thus there is no difference between using width or height. + v = gravity * node.mass * sourceBody.mass / (r * r * r); + sourceBody.force.x += v * dx; + sourceBody.force.y += v * dy; + } else { + // Otherwise, run the procedure recursively on each of the current node's children. + + // I intentionally unfolded this loop, to save several CPU cycles. + if (node.quads[0]) { queue[pushIdx] = node.quads[0]; queueLength += 1; pushIdx += 1; } + if (node.quads[1]) { queue[pushIdx] = node.quads[1]; queueLength += 1; pushIdx += 1; } + if (node.quads[2]) { queue[pushIdx] = node.quads[2]; queueLength += 1; pushIdx += 1; } + if (node.quads[3]) { queue[pushIdx] = node.quads[3]; queueLength += 1; pushIdx += 1; } + } + } + } + }, + + insertBodies = function (bodies) { + var x1 = Number.MAX_VALUE, + y1 = Number.MAX_VALUE, + x2 = Number.MIN_VALUE, + y2 = Number.MIN_VALUE, + i, + max = bodies.length; + + // To reduce quad tree depth we are looking for exact bounding box of all particles. + i = max; + while (i--) { + var x = bodies[i].pos.x; + var y = bodies[i].pos.y; + if (x < x1) { x1 = x; } + if (x > x2) { x2 = x; } + if (y < y1) { y1 = y; } + if (y > y2) { y2 = y; } + } + + // Squarify the bounds. + var dx = x2 - x1, + dy = y2 - y1; + if (dx > dy) { y2 = y1 + dx; } else { x2 = x1 + dy; } + + currentInCache = 0; + root = newNode(); + root.left = x1; + root.right = x2; + root.top = y1; + root.bottom = y2; + + i = max - 1; + if (i > 0) { + root.body = bodies[i]; + } + while (i--) { + insert(bodies[i], root); + } + }; + + return { + insertBodies : insertBodies, + updateBodyForce : update, + options : function (newOptions) { + if (newOptions) { + if (typeof newOptions.gravity === 'number') { gravity = newOptions.gravity; } + if (typeof newOptions.theta === 'number') { theta = newOptions.theta; } + + return this; + } + + return {gravity : gravity, theta : theta}; + } + }; +}; + + +},{"./insertStack":23,"./isSamePosition":24,"./node":25,"ngraph.random":26}],23:[function(require,module,exports){ +module.exports = InsertStack; + +/** + * Our implmentation of QuadTree is non-recursive (recursion handled not really + * well in old browsers). This data structure represent stack of elemnts + * which we are trying to insert into quad tree. It also avoids unnecessary + * memory pressue when we are adding more elements + */ +function InsertStack () { + this.stack = []; + this.popIdx = 0; +} + +InsertStack.prototype = { + isEmpty: function() { + return this.popIdx === 0; + }, + push: function (node, body) { + var item = this.stack[this.popIdx]; + if (!item) { + // we are trying to avoid memory pressue: create new element + // only when absolutely necessary + this.stack[this.popIdx] = new InsertStackElement(node, body); + } else { + item.node = node; + item.body = body; + } + ++this.popIdx; + }, + pop: function () { + if (this.popIdx > 0) { + return this.stack[--this.popIdx]; + } + }, + reset: function () { + this.popIdx = 0; + } +}; + +function InsertStackElement(node, body) { + this.node = node; // QuadTree node + this.body = body; // physical body which needs to be inserted to node +} + +},{}],24:[function(require,module,exports){ +module.exports = function isSamePosition(point1, point2) { + var dx = Math.abs(point1.x - point2.x); + var dy = Math.abs(point1.y - point2.y); + + return (dx < 1e-8 && dy < 1e-8); +}; + +},{}],25:[function(require,module,exports){ +/** + * Internal data structure to represent 2D QuadTree node + */ +module.exports = function Node() { + // body stored inside this node. In quad tree only leaf nodes (by construction) + // contain boides: + this.body = null; + + // Child nodes are stored in quads. Each quad is presented by number: + // 0 | 1 + // ----- + // 2 | 3 + this.quads = []; + + // Total mass of current node + this.mass = 0; + + // Center of mass coordinates + this.massX = 0; + this.massY = 0; + + // bounding box coordinates + this.left = 0; + this.top = 0; + this.bottom = 0; + this.right = 0; + + // Node is internal when it is not a leaf + this.isInternal = false; +}; + +},{}],26:[function(require,module,exports){ +module.exports=require(14) +},{}],27:[function(require,module,exports){ +/** + * # JSON Storage + * + * JSON storage is the most readable and simple graph storage format. Main + * drawback of this format is its size - for large graph due to verbosity + * of the format it can take more space then it could. + * + * Example: + * ``` + * // save file to JSON string: + * var storedGraph = save(graph); + * // Load graph from previously saved string + * var loadedGraph = load(storedGraph); + * // loadedGraph is a copy of original `graph` + * ``` + */ + +module.exports = { + save: save, + load: load +}; + +// Save +// ---- +// Graph is saved as a JSON object and returned as a string. +function save(graph) { + // Object contains `nodes` and `links` arrays. + var result = { + nodes: [], + links: [] + }; + + graph.forEachNode(function (node) { + // Each node of the graph is processed to take only required fields + // `id` and `data` + result.nodes.push(transformNodeForSave(node)); + }); + + graph.forEachLink(function (link) { + // Each link of the graph is also processed to take `fromId`, `toId` and + // `data` + result.links.push(transformLinkForSave(link)); + }); + + return JSON.stringify(result); +} + +function transformNodeForSave(node) { + var result = { id: node.id }; + // We don't want to store undefined fields when it's not necessary: + if (node.data !== undefined) { + result.data = node.data; + } + + return result; +} + +function transformLinkForSave(link) { + var result = { + fromId: link.fromId, + toId: link.toId, + }; + + if (link.data !== undefined) { + result.data = link.data; + } + + return result; +} + +// Load +// ---- +// +// To load previously stored graph, simply call `load()`. +function load(jsonGraph) { + if (typeof jsonGraph !== 'string') { + throw new Error('Cannot load graph which is not stored as a string'); + } + var stored = JSON.parse(jsonGraph), + graph = require('ngraph.graph')(), + i; + + // OPENOIL HACK + // stored_data = JSON.parse(jsonGraph), + // stored = stored_data.data[0][0]; + + if (stored.links === undefined || stored.nodes === undefined) { + throw new Error('Cannot load graph without links and nodes'); + } + + for (i = 0; i < stored.nodes.length; ++i) { + var parsedNode = stored.nodes[i]; + if (!parsedNode.hasOwnProperty('id')) { + throw new Error('Graph node format is invalid: Node id is missing'); + } + + graph.addNode(parsedNode.id, parsedNode.data); + } + + for (i = 0; i < stored.links.length; ++i) { + var link = stored.links[i]; + if (!link.hasOwnProperty('fromId') || !link.hasOwnProperty('toId')) { + throw 'Graph link format is invalid. Both fromId and toId are required'; + } + + graph.addLink(link.fromId, link.toId, link.data); + } + + return graph; +} + +},{"ngraph.graph":28}],28:[function(require,module,exports){ +/** + * @fileOverview Contains definition of the core graph object. + */ + + +/** + * @example + * var graph = require('ngraph.graph')(); + * graph.addNode(1); // graph has one node. + * graph.addLink(2, 3); // now graph contains three nodes and one link. + * + */ +module.exports = function () { + // Graph structure is maintained as dictionary of nodes + // and array of links. Each node has 'links' property which + // hold all links related to that node. And general links + // array is used to speed up all links enumeration. This is inefficient + // in terms of memory, but simplifies coding. + + var nodes = typeof Object.create === 'function' ? Object.create(null) : {}, + links = [], + // Hash of multi-edges. Used to track ids of edges between same nodes + multiEdges = {}, + nodesCount = 0, + suspendEvents = 0, + + // Accumlates all changes made during graph updates. + // Each change element contains: + // changeType - one of the strings: 'add', 'remove' or 'update'; + // node - if change is related to node this property is set to changed graph's node; + // link - if change is related to link this property is set to changed graph's link; + changes = [], + + fireGraphChanged = function (graph) { + graph.fire('changed', changes); + }, + + // Enter, Exit Mofidication allows bulk graph updates without firing events. + enterModification = function () { + suspendEvents += 1; + }, + + exitModification = function (graph) { + suspendEvents -= 1; + if (suspendEvents === 0 && changes.length > 0) { + fireGraphChanged(graph); + changes.length = 0; + } + }, + + recordNodeChange = function (node, changeType) { + changes.push({node : node, changeType : changeType}); + }, + + recordLinkChange = function (link, changeType) { + changes.push({link : link, changeType : changeType}); + }, + linkConnectionSymbol = '👉 '; + + var graphPart = { + + /** + * Adds node to the graph. If node with given id already exists in the graph + * its data is extended with whatever comes in 'data' argument. + * + * @param nodeId the node's identifier. A string or number is preferred. + * note: Node id should not contain 'linkConnectionSymbol'. This will break link identifiers + * @param [data] additional data for the node being added. If node already + * exists its data object is augmented with the new one. + * + * @return {node} The newly added node or node with given id if it already exists. + */ + addNode : function (nodeId, data) { + if (typeof nodeId === 'undefined') { + throw new Error('Invalid node identifier'); + } + + enterModification(); + + var node = this.getNode(nodeId); + if (!node) { + // TODO: Should I check for linkConnectionSymbol here? + node = new Node(nodeId); + nodesCount++; + + recordNodeChange(node, 'add'); + } else { + recordNodeChange(node, 'update'); + } + + node.data = data; + + nodes[nodeId] = node; + + exitModification(this); + return node; + }, + + /** + * Adds a link to the graph. The function always create a new + * link between two nodes. If one of the nodes does not exists + * a new node is created. + * + * @param fromId link start node id; + * @param toId link end node id; + * @param [data] additional data to be set on the new link; + * + * @return {link} The newly created link + */ + addLink : function (fromId, toId, data) { + enterModification(); + + var fromNode = this.getNode(fromId) || this.addNode(fromId); + var toNode = this.getNode(toId) || this.addNode(toId); + + var linkId = fromId.toString() + linkConnectionSymbol + toId.toString(); + var isMultiEdge = multiEdges.hasOwnProperty(linkId); + if (isMultiEdge || this.hasLink(fromId, toId)) { + if (!isMultiEdge) { + multiEdges[linkId] = 0; + } + linkId += '@' + (++multiEdges[linkId]); + } + + var link = new Link(fromId, toId, data, linkId); + + links.push(link); + + // TODO: this is not cool. On large graphs potentially would consume more memory. + fromNode.links.push(link); + toNode.links.push(link); + + recordLinkChange(link, 'add'); + + exitModification(this); + + return link; + }, + + /** + * Removes link from the graph. If link does not exist does nothing. + * + * @param link - object returned by addLink() or getLinks() methods. + * + * @returns true if link was removed; false otherwise. + */ + removeLink : function (link) { + if (!link) { return false; } + var idx = indexOfElementInArray(link, links); + if (idx < 0) { return false; } + + enterModification(); + + links.splice(idx, 1); + + var fromNode = this.getNode(link.fromId); + var toNode = this.getNode(link.toId); + + if (fromNode) { + idx = indexOfElementInArray(link, fromNode.links); + if (idx >= 0) { + fromNode.links.splice(idx, 1); + } + } + + if (toNode) { + idx = indexOfElementInArray(link, toNode.links); + if (idx >= 0) { + toNode.links.splice(idx, 1); + } + } + + recordLinkChange(link, 'remove'); + + exitModification(this); + + return true; + }, + + /** + * Removes node with given id from the graph. If node does not exist in the graph + * does nothing. + * + * @param nodeId node's identifier passed to addNode() function. + * + * @returns true if node was removed; false otherwise. + */ + removeNode: function (nodeId) { + var node = this.getNode(nodeId); + if (!node) { return false; } + + enterModification(); + + while (node.links.length) { + var link = node.links[0]; + this.removeLink(link); + } + + delete nodes[nodeId]; + nodesCount--; + + recordNodeChange(node, 'remove'); + + exitModification(this); + + return true; + }, + + /** + * Gets node with given identifier. If node does not exist undefined value is returned. + * + * @param nodeId requested node identifier; + * + * @return {node} in with requested identifier or undefined if no such node exists. + */ + getNode : function (nodeId) { + return nodes[nodeId]; + }, + + /** + * Gets number of nodes in this graph. + * + * @return number of nodes in the graph. + */ + getNodesCount : function () { + return nodesCount; + }, + + /** + * Gets total number of links in the graph. + */ + getLinksCount : function () { + return links.length; + }, + + /** + * Gets all links (inbound and outbound) from the node with given id. + * If node with given id is not found null is returned. + * + * @param nodeId requested node identifier. + * + * @return Array of links from and to requested node if such node exists; + * otherwise null is returned. + */ + getLinks : function (nodeId) { + var node = this.getNode(nodeId); + return node ? node.links : null; + }, + + /** + * Invokes callback on each node of the graph. + * + * @param {Function(node)} callback Function to be invoked. The function + * is passed one argument: visited node. + */ + forEachNode : function (callback) { + if (typeof callback !== 'function') { + return; + } + var node; + + for (node in nodes) { + if (callback(nodes[node])) { + return; // client doesn't want to proceed. return. + } + } + }, + + /** + * Invokes callback on every linked (adjacent) node to the given one. + * + * @param nodeId Identifier of the requested node. + * @param {Function(node, link)} callback Function to be called on all linked nodes. + * The function is passed two parameters: adjacent node and link object itself. + * @param oriented if true graph treated as oriented. + */ + forEachLinkedNode : function (nodeId, callback, oriented) { + var node = this.getNode(nodeId), + i, + link, + linkedNodeId; + + if (node && node.links && typeof callback === 'function') { + // Extraced orientation check out of the loop to increase performance + if (oriented) { + for (i = 0; i < node.links.length; ++i) { + link = node.links[i]; + if (link.fromId === nodeId) { + callback(nodes[link.toId], link); + } + } + } else { + for (i = 0; i < node.links.length; ++i) { + link = node.links[i]; + linkedNodeId = link.fromId === nodeId ? link.toId : link.fromId; + + callback(nodes[linkedNodeId], link); + } + } + } + }, + + /** + * Enumerates all links in the graph + * + * @param {Function(link)} callback Function to be called on all links in the graph. + * The function is passed one parameter: graph's link object. + * + * Link object contains at least the following fields: + * fromId - node id where link starts; + * toId - node id where link ends, + * data - additional data passed to graph.addLink() method. + */ + forEachLink : function (callback) { + var i, length; + if (typeof callback === 'function') { + for (i = 0, length = links.length; i < length; ++i) { + callback(links[i]); + } + } + }, + + /** + * Suspend all notifications about graph changes until + * endUpdate is called. + */ + beginUpdate : function () { + enterModification(); + }, + + /** + * Resumes all notifications about graph changes and fires + * graph 'changed' event in case there are any pending changes. + */ + endUpdate : function () { + exitModification(this); + }, + + /** + * Removes all nodes and links from the graph. + */ + clear : function () { + var that = this; + that.beginUpdate(); + that.forEachNode(function (node) { that.removeNode(node.id); }); + that.endUpdate(); + }, + + /** + * Detects whether there is a link between two nodes. + * Operation complexity is O(n) where n - number of links of a node. + * + * @returns link if there is one. null otherwise. + */ + hasLink : function (fromNodeId, toNodeId) { + // TODO: Use adjacency matrix to speed up this operation. + var node = this.getNode(fromNodeId), + i; + if (!node) { + return null; + } + + for (i = 0; i < node.links.length; ++i) { + var link = node.links[i]; + if (link.fromId === fromNodeId && link.toId === toNodeId) { + return link; + } + } + + return null; // no link. + } + }; + + // Let graph fire events before we return it to the caller. + var eventify = require('ngraph.events'); + eventify(graphPart); + + return graphPart; +}; + +// need this for old browsers. Should this be a separate module? +function indexOfElementInArray(element, array) { + if (array.indexOf) { + return array.indexOf(element); + } + + var len = array.length, + i; + + for (i = 0; i < len; i += 1) { + if (array[i] === element) { + return i; + } + } + + return -1; +} + +/** + * Internal structure to represent node; + */ +function Node(id) { + this.id = id; + this.links = []; + this.data = null; +} + + +/** + * Internal structure to represent links; + */ +function Link(fromId, toId, data, id) { + this.fromId = fromId; + this.toId = toId; + this.data = data; + this.id = id; +} + +},{"ngraph.events":29}],29:[function(require,module,exports){ +module.exports=require(8) +},{}],30:[function(require,module,exports){ +/*! + query-string + Parse and stringify URL query strings + https://github.com/sindresorhus/query-string + by Sindre Sorhus + MIT License +*/ +(function () { + 'use strict'; + var queryString = {}; + + queryString.parse = function (str) { + if (typeof str !== 'string') { + return {}; + } + + str = str.trim().replace(/^\?/, ''); + + if (!str) { + return {}; + } + + return str.trim().split('&').reduce(function (ret, param) { + var parts = param.replace(/\+/g, ' ').split('='); + // missing `=` should be `null`: + // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters + ret[parts[0]] = parts[1] === undefined ? null : decodeURIComponent(parts[1]); + return ret; + }, {}); + }; + + queryString.stringify = function (obj) { + return obj ? Object.keys(obj).map(function (key) { + return encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]); + }).join('&') : ''; + }; + + if (typeof module !== 'undefined' && module.exports) { + module.exports = queryString; + } else { + window.queryString = queryString; + } +})(); + +},{}]},{},[1]) +(1) +}); \ No newline at end of file diff --git a/pixi/doItFromNode.js b/pixi/doItFromNode.js new file mode 100644 index 0000000..8114974 --- /dev/null +++ b/pixi/doItFromNode.js @@ -0,0 +1,53 @@ +// This example renders graph into an image +var graph = require('ngraph.generators').grid(10, 10); + +// Perform 500 iterations of graph layout: +var layout = layoutGraph(graph, 500); + +// Ask fabric to render graph with given layout into a canvas +var canvas = renderToCanvas(graph, layout); + +// And finally save it to a file +saveCanvasToFile(canvas, 'outGraph.png'); + +function layoutGraph(graph, iterationsCount) { + // we are going to use our own layout: + var layout = require('ngraph.forcelayout')(graph); + console.log('Running layout...'); + for (var i = 0; i < iterationsCount; ++i) { + layout.step(); + } + console.log('Done. Rendering graph...'); + return layout; +} + +function renderToCanvas(graph, layout) { + var graphRect = layout.getGraphRect(); + var size = Math.max(graphRect.x2 - graphRect.x1, graphRect.y2 - graphRect.y1) + 200; + + var fabricGraphics = require('ngraph.fabric')(graph, { width: size, height: size, layout: layout }); + var fabric = require('fabric').fabric; + + // This line customize appearance of each node and link. The best part of it - + // it is the same code which renders graph in `index.js` + require('./ui')(fabricGraphics, fabric); + + var scale = 1; + fabricGraphics.setTransform(size/2, size/2, scale); + fabricGraphics.renderOneFrame(); // One frame is enough + + return fabricGraphics.canvas; +} + +function saveCanvasToFile(canvas, fileName) { + var fs = require('fs'); + var path = require('path'); + var fullName = path.join(__dirname, fileName); + var outFile = fs.createWriteStream(fullName); + + canvas.createPNGStream().on('data', function(chunk) { + outFile.write(chunk); + }).on('end', function () { + console.log('Graph saved to: ' + fullName); + }); +} diff --git a/pixi/index.html b/pixi/index.html new file mode 100644 index 0000000..2c6144f --- /dev/null +++ b/pixi/index.html @@ -0,0 +1,14 @@ + + + + Fabric.js - custom UI + + + + + + + + diff --git a/pixi/index.js b/pixi/index.js new file mode 100644 index 0000000..ae26ab8 --- /dev/null +++ b/pixi/index.js @@ -0,0 +1,49 @@ +module.exports.main = function () { +// var graph = getGraphFromQueryString(window.location.search.substring(1)); + +// This collection is huge. Import concrete graph only +// to reduce amount of browserified package: + +fs = require('fs'); + +var jsonParser = require('ngraph.serialization/json'), + json_results = fs.readFileSync('../../ngraph_results.json', 'utf8') + + var graph = jsonParser.load(json_results); + + var createPixiGraphics = require('ngraph.pixi'); + + var pixiGraphics = createPixiGraphics(graph, { + background: 0xFFFFFF, + + physics: { + springLength: 30, + springCoeff: 0.0008, + dragCoeff: 0.01, + gravity: -1.2, + } + }); + + // setup our custom looking nodes and links: + pixiGraphics.createNodeUI(require('./lib/createNodeUI')) + .renderNode(require('./lib/renderNode')) + .createLinkUI(require('./lib/createLinkUI')) + .renderLink(require('./lib/renderLink')); + + // just make sure first node does not move: + var layout = pixiGraphics.layout; + layout.pinNode(graph.getNode('bp_p_l_c'), true); + + // begin animation loop: + pixiGraphics.run(); +}; + +function getGraphFromQueryString(queryString) { + var query = require('query-string').parse(queryString); + var n = parseInt(query.n, 10) || 10; + var m = parseInt(query.m, 10) || 10; + + var graphGenerators = require('ngraph.generators'); + var createGraph = graphGenerators[query.graph] || graphGenerators.grid; + return createGraph(n, m); +} diff --git a/pixi/lib/createLinkUI.js b/pixi/lib/createLinkUI.js new file mode 100644 index 0000000..32efa37 --- /dev/null +++ b/pixi/lib/createLinkUI.js @@ -0,0 +1,7 @@ +module.exports = function (link) { + return { + width: 3, + color: 0x33FF33, + immediate: link.immediate + } +}; diff --git a/pixi/lib/createNodeUI.js b/pixi/lib/createNodeUI.js new file mode 100644 index 0000000..285d687 --- /dev/null +++ b/pixi/lib/createNodeUI.js @@ -0,0 +1,7 @@ +module.exports = function (node) { + return { + width: 10, + color: 0x33FF33, + name: node.name + } +}; diff --git a/pixi/lib/renderLink.js b/pixi/lib/renderLink.js new file mode 100644 index 0000000..2ab4faf --- /dev/null +++ b/pixi/lib/renderLink.js @@ -0,0 +1,5 @@ +module.exports = function (link, ctx) { + ctx.lineStyle(1, link.color); + ctx.moveTo(link.from.x, link.from.y); + ctx.lineTo(link.to.x, link.to.y); + } \ No newline at end of file diff --git a/pixi/lib/renderNode.js b/pixi/lib/renderNode.js new file mode 100644 index 0000000..677510c --- /dev/null +++ b/pixi/lib/renderNode.js @@ -0,0 +1,8 @@ +module.exports = function (node, ctx) { + ctx.lineStyle(0); + ctx.beginFill(node.color); + var x = node.pos.x - node.width/2, + y = node.pos.y - node.width/2; + + ctx.drawRect(x, y, node.width, node.width); + } \ No newline at end of file diff --git a/pixi/package.json b/pixi/package.json new file mode 100644 index 0000000..ae77c69 --- /dev/null +++ b/pixi/package.json @@ -0,0 +1,20 @@ +{ + "scripts": { + "start": "node_modules/.bin/browserify -t brfs -s ngraph index.js > bundle.js" + }, + "dependencies": { + "brfs": "^1.1.1", + "fabric": "~1.4.3", + "fs": "0.0.1", + "ngraph.fabric": "~0.0", + "ngraph.forcelayout": "~0.0", + "ngraph.generators": "~0.0", + "ngraph.pixi": "0.0.1", + "ngraph.serialization": "0.0.3", + "ngraph.sparse-collection": "0.0.2", + "query-string": "~0.1.1" + }, + "devDependencies": { + "browserify": "~3.18.0" + } +} diff --git a/pixi/ui.js b/pixi/ui.js new file mode 100644 index 0000000..a6ccfd1 --- /dev/null +++ b/pixi/ui.js @@ -0,0 +1,55 @@ +// this code changes appearance of nodes and links for fabricGraphics. +// It is used in `index.js` to render interactive graphs in a browser +// and in `doItFromNode` example to save graph as image from node.js +module.exports = function (fabricGraphics, fabric) { + fabricGraphics.createNodeUI(createNode) + .renderNode(renderNode) + .createLinkUI(createLink) + .renderLink(renderLink); + + return; + + function createNode(node) { + return new fabric.Circle({ radius: Math.random() * 20, fill: getNiceColor() }); + } + + function renderNode(circle) { + circle.left = circle.pos.x - circle.radius; + circle.top = circle.pos.y - circle.radius; + } + + function createLink(link) { + // lines in fabric are odd... Probably I don't understand them. + return new fabric.Line([0, 0, 0, 0], { + stroke: getNiceColor(), + originX: 'center', + originY: 'center' + }); + } + + function renderLink(line) { + line.set({ + x1: line.from.x, + y1: line.from.y, + x2: line.to.x, + y2: line.to.y + }); + } +} + +var niceColors = [ + '#1f77b4', '#aec7e8', + '#ff7f0e', '#ffbb78', + '#2ca02c', '#98df8a', + '#d62728', '#ff9896', + '#9467bd', '#c5b0d5', + '#8c564b', '#c49c94', + '#e377c2', '#f7b6d2', + '#7f7f7f', '#c7c7c7', + '#bcbd22', '#dbdb8d', + '#17becf', '#9edae5' +]; + +function getNiceColor() { + return niceColors[(Math.random() * niceColors.length)|0]; +}