diff --git a/dist/melonjs.js b/dist/melonjs.js new file mode 100644 index 0000000000..5f7ff2ae51 --- /dev/null +++ b/dist/melonjs.js @@ -0,0 +1,32078 @@ +/*! + * melonJS Game Engine - v7.0.0 + * http://www.melonjs.org + * melonjs is licensed under the MIT License. + * http://www.opensource.org/licenses/mit-license + * @copyright (C) 2011 - 2019 Olivier Biot + */ +(function () { + 'use strict'; + + /* eslint-disable no-undef */ + (function (global) { + /** + * (m)elonJS (e)ngine : All melonJS functions are defined inside + * of this namespace. + *
You generally should not add new properties to this namespace as it may be + * overwritten in future versions.
+ * @name me + * @namespace + */ + + var me = {}; // support for AMD (Asynchronous Module Definition) libraries + + if (typeof define === "function" && define.amd) { + define([], function () { + return { + me: me + }; + }); + } // CommonJS and Node.js module support. + else if (typeof exports !== "undefined") { + // Support Node.js specific `module.exports` (which can be a function) + if (typeof module !== "undefined" && module.exports) { + exports = module.exports = me; + } // CommonJS module 1.1.1 spec (`exports` cannot be a function) + + + exports.me = me; + } // in case AMD not available or unused + + + if (typeof window !== "undefined") { + window.me = me; + } else if (typeof global !== "undefined") { + // Add to global in Node.js (for testing, etc). + global.me = me; + } + })(window); + /* eslint-enable no-undef */ + + /* eslint-disable no-global-assign, no-native-reassign */ + (function () { + if (typeof console === "undefined") { + /** + * Dummy console.log to avoid crash + * in case the browser does not support it + * @ignore + */ + console = { + log: function log() {}, + info: function info() {}, + error: function error() { + alert(Array.prototype.slice.call(arguments).join(", ")); + } + }; + } // based on the requestAnimationFrame polyfill by Erik Möller + + + (function () { + var lastTime = 0; + var frameDuration = 1000 / 60; + var vendors = ["ms", "moz", "webkit", "o"]; + var x; // standardized functions + // https://developer.mozilla.org/fr/docs/Web/API/Window/requestAnimationFrame + + var requestAnimationFrame = window.requestAnimationFrame; + var cancelAnimationFrame = window.cancelAnimationFrame; // get prefixed rAF and cAF is standard one not supported + + for (x = 0; x < vendors.length && !requestAnimationFrame; ++x) { + requestAnimationFrame = window[vendors[x] + "RequestAnimationFrame"]; + } + + for (x = 0; x < vendors.length && !cancelAnimationFrame; ++x) { + cancelAnimationFrame = window[vendors[x] + "CancelAnimationFrame"] || window[vendors[x] + "CancelRequestAnimationFrame"]; + } + + if (!requestAnimationFrame || !cancelAnimationFrame) { + requestAnimationFrame = function requestAnimationFrame(callback) { + var currTime = window.performance.now(); + var timeToCall = Math.max(0, frameDuration - (currTime - lastTime)); + var id = window.setTimeout(function () { + callback(currTime + timeToCall); + }, timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + + cancelAnimationFrame = function cancelAnimationFrame(id) { + window.clearTimeout(id); + }; // put back in global namespace + + + window.requestAnimationFrame = requestAnimationFrame; + window.cancelAnimationFrame = cancelAnimationFrame; + } + })(); + })(); + /* eslint-enable no-global-assign, no-native-reassign */ + + var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + + function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; + } + + var jayExtend = createCommonjsModule(function (module) { + /** + * Extend a class prototype with the provided mixin descriptors. + * Designed as a faster replacement for John Resig's Simple Inheritance. + * @name extend + * @memberOf Jay + * @function + * @param {Object[]} mixins... Each mixin is a dictionary of functions, or a + * previously extended class whose methods will be applied to the target class + * prototype. + * @return {Object} + * @example + * var Person = Jay.extend({ + * "init" : function (isDancing) { + * this.dancing = isDancing; + * }, + * "dance" : function () { + * return this.dancing; + * } + * }); + * + * var Ninja = Person.extend({ + * "init" : function () { + * // Call the super constructor, passing a single argument + * this._super(Person, "init", [false]); + * }, + * "dance" : function () { + * // Call the overridden dance() method + * return this._super(Person, "dance"); + * }, + * "swingSword" : function () { + * return true; + * } + * }); + * + * var Pirate = Person.extend(Ninja, { + * "init" : function () { + * // Call the super constructor, passing a single argument + * this._super(Person, "init", [true]); + * } + * }); + * + * var p = new Person(true); + * console.log(p.dance()); // => true + * + * var n = new Ninja(); + * console.log(n.dance()); // => false + * console.log(n.swingSword()); // => true + * + * var r = new Pirate(); + * console.log(r.dance()); // => true + * console.log(r.swingSword()); // => true + * + * console.log( + * p instanceof Person && + * n instanceof Ninja && + * n instanceof Person && + * r instanceof Pirate && + * r instanceof Person + * ); // => true + * + * console.log(r instanceof Ninja); // => false + */ + (function () { + function extend() { + var methods = {}; + var mixins = new Array(arguments.length); + for (var i = 0; i < arguments.length; i++) { + mixins.push(arguments[i]); + } + + /** + * The class constructor which calls the user `init` constructor. + * @ignore + */ + function Class() { + // Call the user constructor + this.init.apply(this, arguments); + return this; + } + + // Apply superClass + Class.prototype = Object.create(this.prototype); + + // Apply all mixin methods to the class prototype + mixins.forEach(function (mixin) { + apply_methods(Class, methods, mixin.__methods__ || mixin); + }); + + // Verify constructor exists + if (!("init" in Class.prototype)) { + throw new TypeError( + "extend: Class is missing a constructor named `init`" + ); + } + + // Apply syntactic sugar for accessing methods on super classes + Object.defineProperty(Class.prototype, "_super", { + "value" : _super + }); + + // Create a hidden property on the class itself + // List of methods, used for applying classes as mixins + Object.defineProperty(Class, "__methods__", { + "value" : methods + }); + + // Make this class extendable + Class.extend = extend; + + return Class; + } + + /** + * Apply methods to the class prototype. + * @ignore + */ + function apply_methods(Class, methods, descriptor) { + Object.keys(descriptor).forEach(function (method) { + methods[method] = descriptor[method]; + + if (typeof(descriptor[method]) !== "function") { + throw new TypeError( + "extend: Method `" + method + "` is not a function" + ); + } + + Object.defineProperty(Class.prototype, method, { + "configurable" : true, + "value" : descriptor[method] + }); + }); + } + + /** + * Special method that acts as a proxy to the super class. + * @name _super + * @ignore + */ + function _super(superClass, method, args) { + return superClass.prototype[method].apply(this, args); + } + + /** + * The base class from which all jay-extend classes inherit. + * @ignore + */ + var Jay = function () { + Object.apply(this, arguments); + }; + Jay.prototype = Object.create(Object.prototype); + Jay.prototype.constructor = Jay; + + Object.defineProperty(Jay, "extend", { + "value" : extend + }); + + /** + * Export the extend method. + * @ignore + */ + if (typeof(window) !== "undefined") { + window.Jay = Jay; + } + else { + module.exports = Jay; + } + })(); + }); + + /** + * The base class from which all melonJS objects inherit. + * See: {@link https://github.com/parasyte/jay-extend} + * @class + * @memberOf me + */ + + me.Object = window.Jay; + + (function () { + /** + * Convert first character of a string to uppercase, if it's a letter. + * @ignore + * @function + * @name capitalize + * @param {String} str Input string. + * @return {String} String with first letter made uppercase. + */ + var capitalize = function capitalize(str) { + return str.substring(0, 1).toUpperCase() + str.substring(1, str.length); + }; + /** + * A collection of utilities to ease porting between different user agents. + * @namespace me.agent + * @memberOf me + */ + + + me.agent = function () { + var api = {}; + /** + * Known agent vendors + * @ignore + */ + + var vendors = ["ms", "MS", "moz", "webkit", "o"]; + /** + * Get a vendor-prefixed property + * @public + * @name prefixed + * @function + * @param {String} name Property name + * @param {Object} [obj=window] Object or element reference to access + * @return {Mixed} Value of property + * @memberOf me.agent + */ + + api.prefixed = function (name, obj) { + obj = obj || window; + + if (name in obj) { + return obj[name]; + } + + var uc_name = capitalize(name); + var result; + vendors.some(function (vendor) { + var name = vendor + uc_name; + return result = name in obj ? obj[name] : undefined; + }); + return result; + }; + /** + * Set a vendor-prefixed property + * @public + * @name setPrefixed + * @function + * @param {String} name Property name + * @param {Mixed} value Property value + * @param {Object} [obj=window] Object or element reference to access + * @return true if one of the vendor-prefixed property was found + * @memberOf me.agent + */ + + + api.setPrefixed = function (name, value, obj) { + obj = obj || window; + + if (name in obj) { + obj[name] = value; + return; + } + + var uc_name = capitalize(name); + vendors.some(function (vendor) { + var name = vendor + uc_name; + + if (name in obj) { + obj[name] = value; + return true; + } + + return false; + }); + }; + + return api; + }(); + })(); + + function _typeof(obj) { + if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { + _typeof = function (obj) { + return typeof obj; + }; + } else { + _typeof = function (obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + } + + return _typeof(obj); + } + + (function () { + /** + * A singleton object representing the device capabilities and specific events + * @namespace me.device + * @memberOf me + */ + me.device = function () { + // defines object for holding public information/functionality. + var api = {}; // private properties + + var accelInitialized = false; + var deviceOrientationInitialized = false; // swipe utility fn & flag + + var swipeEnabled = true; + + var disableSwipeFn = function disableSwipeFn(e) { + e.preventDefault(); + + if (typeof window.scroll === "function") { + window.scroll(0, 0); + } + + return false; + }; + /* + * DOM loading stuff + */ + + + var readyBound = false, + isReady = false, + readyList = []; + /** + * called to check if the device is ready + * @ignore + */ + + api._domReady = function (fn) { + // Make sure that the DOM is not already loaded + if (!isReady) { + // be sure document.body is there + if (!document.body) { + return setTimeout(me.device._domReady, 13); + } // clean up loading event + + + if (document.removeEventListener) { + document.removeEventListener("DOMContentLoaded", me.device._domReady, false); + } // remove the event on window.onload (always added in `onReady`) + + + window.removeEventListener("load", me.device._domReady, false); // execute all callbacks + + while (readyList.length) { + readyList.shift().call(window, []); + } // Remember that the DOM is ready + + + isReady = true; + } + }; + /** + * check the device capapbilities + * @ignore + */ + + + api._check = function () { + // detect device type/platform + me.device._detectDevice(); // Mobile browser hacks + + + if (me.device.isMobile) { + // Prevent the webview from moving on a swipe + api.enableSwipe(false); + } // Touch/Gesture Event feature detection + + + me.device.TouchEvent = !!("ontouchstart" in window); + me.device.PointerEvent = !!window.PointerEvent; + window.gesture = me.agent.prefixed("gesture"); // detect touch capabilities + + me.device.touch = me.device.TouchEvent || me.device.PointerEvent; // max amount of touch points ; always at least return 1 (e.g. headless chrome will return 0) + + me.device.maxTouchPoints = me.device.touch ? me.device.PointerEvent ? navigator.maxTouchPoints || 1 : 10 : 1; // detect wheel event support + // Modern browsers support "wheel", Webkit and IE support at least "mousewheel + + me.device.wheel = "onwheel" in document.createElement("div"); // accelerometer detection + + me.device.hasAccelerometer = typeof window.DeviceMotionEvent !== "undefined"; // pointerlock detection + + this.hasPointerLockSupport = me.agent.prefixed("pointerLockElement", document); + + if (this.hasPointerLockSupport) { + document.exitPointerLock = me.agent.prefixed("exitPointerLock", document); + } // device orientation and motion detection + + + if (window.DeviceOrientationEvent) { + me.device.hasDeviceOrientation = true; + } // fullscreen api detection & polyfill when possible + + + this.hasFullscreenSupport = me.agent.prefixed("fullscreenEnabled", document) || document.mozFullScreenEnabled; + document.exitFullscreen = me.agent.prefixed("cancelFullScreen", document) || me.agent.prefixed("exitFullscreen", document); // vibration API poyfill + + navigator.vibrate = me.agent.prefixed("vibrate", navigator); // web Audio detection + + this.hasWebAudio = !!(window.AudioContext || window.webkitAudioContext); + + try { + api.localStorage = !!window.localStorage; + } catch (e) { + // the above generates an exception when cookies are blocked + api.localStorage = false; + } // set pause/stop action on losing focus + + + window.addEventListener("blur", function () { + if (me.sys.stopOnBlur) { + me.state.stop(true); + } + + if (me.sys.pauseOnBlur) { + me.state.pause(true); + } + }, false); // set restart/resume action on gaining focus + + window.addEventListener("focus", function () { + if (me.sys.stopOnBlur) { + me.state.restart(true); + } + + if (me.sys.resumeOnFocus) { + me.state.resume(true); + } // force focus if autofocus is on + + + if (me.sys.autoFocus) { + me.device.focus(); + } + }, false); // Set the name of the hidden property and the change event for visibility + + var hidden, visibilityChange; + + if (typeof document.hidden !== "undefined") { + // Opera 12.10 and Firefox 18 and later support + hidden = "hidden"; + visibilityChange = "visibilitychange"; + } else if (typeof document.mozHidden !== "undefined") { + hidden = "mozHidden"; + visibilityChange = "mozvisibilitychange"; + } else if (typeof document.msHidden !== "undefined") { + hidden = "msHidden"; + visibilityChange = "msvisibilitychange"; + } else if (typeof document.webkitHidden !== "undefined") { + hidden = "webkitHidden"; + visibilityChange = "webkitvisibilitychange"; + } // register on the event if supported + + + if (typeof visibilityChange === "string") { + // add the corresponding event listener + document.addEventListener(visibilityChange, function () { + if (document[hidden]) { + if (me.sys.stopOnBlur) { + me.state.stop(true); + } + + if (me.sys.pauseOnBlur) { + me.state.pause(true); + } + } else { + if (me.sys.stopOnBlur) { + me.state.restart(true); + } + + if (me.sys.resumeOnFocus) { + me.state.resume(true); + } + } + }, false); + } + }; + /** + * detect the device type + * @ignore + */ + + + api._detectDevice = function () { + // iOS Device ? + me.device.iOS = /iPhone|iPad|iPod/i.test(me.device.ua); // Android Device ? + + me.device.android = /Android/i.test(me.device.ua); + me.device.android2 = /Android 2/i.test(me.device.ua); // Linux platform + + me.device.linux = /Linux/i.test(me.device.ua); // Chrome OS ? + + me.device.chromeOS = /CrOS/.test(me.device.ua); // Windows Device ? + + me.device.wp = /Windows Phone/i.test(me.device.ua); // Blackberry device ? + + me.device.BlackBerry = /BlackBerry/i.test(me.device.ua); // Kindle device ? + + me.device.Kindle = /Kindle|Silk.*Mobile Safari/i.test(me.device.ua); // Mobile platform + + me.device.isMobile = /Mobi/i.test(me.device.ua) || me.device.iOS || me.device.android || me.device.wp || me.device.BlackBerry || me.device.Kindle || false; // ejecta + + me.device.ejecta = typeof window.ejecta !== "undefined"; // Wechat + + me.device.isWeixin = /MicroMessenger/i.test(me.device.ua); + }; + /* + * PUBLIC Properties & Functions + */ + // Browser capabilities + + /** + * the `ua` read-only property returns the user agent string for the current browser. + * @type String + * @readonly + * @name ua + * @memberOf me.device + */ + + + api.ua = navigator.userAgent; + /** + * Browser Local Storage capabilities
+ * There is no constructor function for me.timer
+ * @namespace me.timer
+ * @memberOf me
+ */
+ me.timer = function () {
+ // hold public stuff in our api
+ var api = {};
+ /*
+ * PRIVATE STUFF
+ */
+ //hold element to display fps
+
+ var framecount = 0;
+ var framedelta = 0;
+ /* fps count stuff */
+
+ var last = 0;
+ var now = 0;
+ var delta = 0;
+ var step = Math.ceil(1000 / me.sys.fps); // ROUND IT ?
+ // define some step with some margin
+
+ var minstep = 1000 / me.sys.fps * 1.25; // IS IT NECESSARY?\
+ // list of defined timer function
+
+ var timers = [];
+ var timerId = 0;
+ /**
+ * @ignore
+ */
+
+ var clearTimer = function clearTimer(timerId) {
+ for (var i = 0, len = timers.length; i < len; i++) {
+ if (timers[i].timerId === timerId) {
+ timers.splice(i, 1);
+ break;
+ }
+ }
+ };
+ /**
+ * update timers
+ * @ignore
+ */
+
+
+ var updateTimers = function updateTimers(dt) {
+ for (var i = 0, len = timers.length; i < len; i++) {
+ var _timer = timers[i];
+
+ if (!(_timer.pauseable && me.state.isPaused())) {
+ _timer.elapsed += dt;
+ }
+
+ if (_timer.elapsed >= _timer.delay) {
+ _timer.fn.apply(null, _timer.args);
+
+ if (_timer.repeat === true) {
+ _timer.elapsed -= _timer.delay;
+ } else {
+ me.timer.clearTimeout(_timer.timerId);
+ }
+ }
+ }
+ };
+ /*
+ * PUBLIC STUFF
+ */
+
+ /**
+ * Last game tick value.
+ * Use this value to scale velocities during frame drops due to slow
+ * hardware or when setting an FPS limit. (See {@link me.sys.fps})
+ * This feature is disabled by default. Enable me.sys.interpolation to
+ * use it.
+ * @public
+ * @see me.sys.interpolation
+ * @type Number
+ * @name tick
+ * @memberOf me.timer
+ */
+
+
+ api.tick = 1.0;
+ /**
+ * Last measured fps rate.
+ * This feature is disabled by default. Load and enable the DebugPanel
+ * plugin to use it.
+ * @public
+ * @type Number
+ * @name fps
+ * @memberOf me.timer
+ */
+
+ api.fps = 0;
+ /**
+ * Last update time.
+ * Use this value to implement frame prediction in drawing events,
+ * for creating smooth motion while running game update logic at
+ * a lower fps.
+ * @public
+ * @type Date
+ * @name lastUpdate
+ * @memberOf me.timer
+ */
+
+ api.lastUpdate = window.performance.now();
+ /**
+ * init the timer
+ * @ignore
+ */
+
+ api.init = function () {
+ // reset variables to initial state
+ api.reset();
+ now = last = 0;
+ };
+ /**
+ * reset time (e.g. usefull in case of pause)
+ * @name reset
+ * @memberOf me.timer
+ * @ignore
+ * @function
+ */
+
+
+ api.reset = function () {
+ // set to "now"
+ last = now = window.performance.now();
+ delta = 0; // reset delta counting variables
+
+ framedelta = 0;
+ framecount = 0;
+ };
+ /**
+ * Calls a function once after a specified delay. See me.timer.setInterval to repeativly call a function.
+ * @name setTimeout
+ * @memberOf me.timer
+ * @param {Function} fn the function you want to execute after delay milliseconds.
+ * @param {Number} delay the number of milliseconds (thousandths of a second) that the function call should be delayed by.
+ * @param {Boolean} [pauseable=true] respects the pause state of the engine.
+ * @param {...*} [param] optional parameters which are passed through to the function specified by fn once the timer expires.
+ * @return {Number} The numerical ID of the timer, which can be used later with me.timer.clearTimeout().
+ * @function
+ * @example
+ * // set a timer to call "myFunction" after 1000ms
+ * me.timer.setTimeout(myFunction, 1000);
+ * // set a timer to call "myFunction" after 1000ms (respecting the pause state) and passing param1 and param2
+ * me.timer.setTimeout(myFunction, 1000, true, param1, param2);
+ */
+
+
+ api.setTimeout = function (fn, delay, pauseable) {
+ timers.push({
+ fn: fn,
+ delay: delay,
+ elapsed: 0,
+ repeat: false,
+ timerId: ++timerId,
+ pauseable: pauseable === true || true,
+ args: arguments.length > 3 ? Array.prototype.slice.call(arguments, 3) : undefined
+ });
+ return timerId;
+ };
+ /**
+ * Calls a function continously at the specified interval. See setTimeout to call function a single time.
+ * @name setInterval
+ * @memberOf me.timer
+ * @param {Function} fn the function to execute
+ * @param {Number} delay the number of milliseconds (thousandths of a second) on how often to execute the function
+ * @param {Boolean} [pauseable=true] respects the pause state of the engine.
+ * @param {...*} [param] optional parameters which are passed through to the function specified by fn once the timer expires.
+ * @return {Number} The numerical ID of the timer, which can be used later with me.timer.clearInterval().
+ * @function
+ * @example
+ * // set a timer to call "myFunction" every 1000ms
+ * me.timer.setInterval(myFunction, 1000);
+ * // set a timer to call "myFunction" every 1000ms (respecting the pause state) and passing param1 and param2
+ * me.timer.setInterval(myFunction, 1000, true, param1, param2);
+ */
+
+
+ api.setInterval = function (fn, delay, pauseable) {
+ timers.push({
+ fn: fn,
+ delay: delay,
+ elapsed: 0,
+ repeat: true,
+ timerId: ++timerId,
+ pauseable: pauseable === true || true,
+ args: arguments.length > 3 ? Array.prototype.slice.call(arguments, 3) : undefined
+ });
+ return timerId;
+ };
+ /**
+ * Clears the delay set by me.timer.setTimeout().
+ * @name clearTimeout
+ * @memberOf me.timer
+ * @function
+ * @param {Number} timeoutID ID of the timeout to be cleared
+ */
+
+
+ api.clearTimeout = function (timeoutID) {
+ me.utils.function.defer(clearTimer, this, timeoutID);
+ };
+ /**
+ * Clears the Interval set by me.timer.setInterval().
+ * @name clearInterval
+ * @memberOf me.timer
+ * @function
+ * @param {Number} intervalID ID of the interval to be cleared
+ */
+
+
+ api.clearInterval = function (intervalID) {
+ me.utils.function.defer(clearTimer, this, intervalID);
+ };
+ /**
+ * Return the current timestamp in milliseconds
+ * since the game has started or since linux epoch (based on browser support for High Resolution Timer)
+ * @name getTime
+ * @memberOf me.timer
+ * @return {Number}
+ * @function
+ */
+
+
+ api.getTime = function () {
+ return now;
+ };
+ /**
+ * Return elapsed time in milliseconds since the last update
+ * @name getDelta
+ * @memberOf me.timer
+ * @return {Number}
+ * @function
+ */
+
+
+ api.getDelta = function () {
+ return delta;
+ };
+ /**
+ * compute the actual frame time and fps rate
+ * @name computeFPS
+ * @ignore
+ * @memberOf me.timer
+ * @function
+ */
+
+
+ api.countFPS = function () {
+ framecount++;
+ framedelta += delta;
+
+ if (framecount % 10 === 0) {
+ this.fps = me.Math.clamp(~~(1000 * framecount / framedelta), 0, me.sys.fps);
+ framedelta = 0;
+ framecount = 0;
+ }
+ };
+ /**
+ * update game tick
+ * should be called once a frame
+ * @param {Number} time current timestamp as provided by the RAF callback
+ * @return {Number} time elapsed since the last update
+ * @ignore
+ */
+
+
+ api.update = function (time) {
+ last = now;
+ now = time;
+ delta = now - last; // fix for negative timestamp returned by wechat or chrome on startup
+
+ if (delta < 0) {
+ delta = 0;
+ } // get the game tick
+
+
+ api.tick = delta > minstep && me.sys.interpolation ? delta / step : 1; // update defined timers
+
+ updateTimers(delta);
+ return delta;
+ }; // return our apiect
+
+
+ return api;
+ }();
+ })();
+
+ (function () {
+ /**
+ * This object is used for object pooling - a technique that might speed up your game if used properly.
+ * If some of your classes will be instantiated and removed a lot at a time, it is a
+ * good idea to add the class to this object pool. A separate pool for that class
+ * will be created, which will reuse objects of the class. That way they won't be instantiated
+ * each time you need a new one (slowing your game), but stored into that pool and taking one
+ * already instantiated when you need it.
+ * This object is also used by the engine to instantiate objects defined in the map,
+ * which means, that on level loading the engine will try to instantiate every object
+ * found in the map, based on the user defined name in each Object Properties
+ *
+ * @namespace me.pool
+ * @memberOf me
+ */
+ me.pool = function () {
+ // hold public stuff in our singleton
+ var api = {};
+ var objectClass = {};
+ var instance_counter = 0;
+ /*
+ * PUBLIC STUFF
+ */
+
+ /**
+ * Constructor
+ * @ignore
+ */
+
+ api.init = function () {
+ api.register("me.Entity", me.Entity);
+ api.register("me.CollectableEntity", me.CollectableEntity);
+ api.register("me.LevelEntity", me.LevelEntity);
+ api.register("me.Tween", me.Tween, true);
+ api.register("me.Color", me.Color, true);
+ api.register("me.Particle", me.Particle, true);
+ api.register("me.Sprite", me.Sprite);
+ api.register("me.Text", me.Text, true);
+ api.register("me.BitmapText", me.BitmapText, true);
+ api.register("me.BitmapTextData", me.BitmapTextData, true);
+ api.register("me.ImageLayer", me.ImageLayer, true);
+ api.register("me.ColorLayer", me.ColorLayer, true);
+ api.register("me.Vector2d", me.Vector2d, true);
+ api.register("me.Vector3d", me.Vector3d, true);
+ api.register("me.ObservableVector2d", me.ObservableVector2d, true);
+ api.register("me.ObservableVector3d", me.ObservableVector3d, true);
+ api.register("me.Matrix2d", me.Matrix2d, true);
+ api.register("me.Rect", me.Rect, true);
+ api.register("me.Polygon", me.Polygon, true);
+ api.register("me.Line", me.Line, true);
+ api.register("me.Ellipse", me.Ellipse, true);
+ };
+ /**
+ * register an object to the pool.
+ * Pooling must be set to true if more than one such objects will be created.
+ * (note) If pooling is enabled, you shouldn't instantiate objects with `new`.
+ * See examples in {@link me.pool#pull}
+ * @name register
+ * @memberOf me.pool
+ * @public
+ * @function
+ * @param {String} className as defined in the Name field of the Object Properties (in Tiled)
+ * @param {Object} class corresponding Class to be instantiated
+ * @param {Boolean} [objectPooling=false] enables object pooling for the specified class
+ * - speeds up the game by reusing existing objects
+ * @example
+ * // add our users defined entities in the object pool
+ * me.pool.register("playerspawnpoint", PlayerEntity);
+ * me.pool.register("cherryentity", CherryEntity, true);
+ * me.pool.register("heartentity", HeartEntity, true);
+ * me.pool.register("starentity", StarEntity, true);
+ */
+
+
+ api.register = function (className, classObj, pooling) {
+ if (typeof classObj !== "undefined") {
+ objectClass[className] = {
+ "class": classObj,
+ "pool": pooling ? [] : undefined
+ };
+ } else {
+ throw new Error("Cannot register object '" + className + "', invalid class");
+ }
+ };
+ /**
+ * Pull a new instance of the requested object (if added into the object pool)
+ * @name pull
+ * @memberOf me.pool
+ * @public
+ * @function
+ * @param {String} className as used in {@link me.pool.register}
+ * @param {} [arguments...] arguments to be passed when instantiating/reinitializing the object
+ * @return {Object} the instance of the requested object
+ * @example
+ * me.pool.register("player", PlayerEntity);
+ * var player = me.pool.pull("player");
+ * @example
+ * me.pool.register("bullet", BulletEntity, true);
+ * me.pool.register("enemy", EnemyEntity, true);
+ * // ...
+ * // when we need to manually create a new bullet:
+ * var bullet = me.pool.pull("bullet", x, y, direction);
+ * // ...
+ * // params aren't a fixed number
+ * // when we need new enemy we can add more params, that the object construct requires:
+ * var enemy = me.pool.pull("enemy", x, y, direction, speed, power, life);
+ * // ...
+ * // when we want to destroy existing object, the remove
+ * // function will ensure the object can then be reallocated later
+ * me.game.world.removeChild(enemy);
+ * me.game.world.removeChild(bullet);
+ */
+
+
+ api.pull = function (name) {
+ var args = new Array(arguments.length);
+
+ for (var i = 0; i < arguments.length; i++) {
+ args[i] = arguments[i];
+ }
+
+ var entity = objectClass[name];
+
+ if (entity) {
+ var proto = entity["class"],
+ pool = entity.pool,
+ obj;
+
+ if (pool && (obj = pool.pop())) {
+ args.shift(); // call the object onResetEvent function if defined
+
+ if (typeof obj.onResetEvent === "function") {
+ obj.onResetEvent.apply(obj, args);
+ } else {
+ obj.init.apply(obj, args);
+ }
+
+ instance_counter--;
+ } else {
+ args[0] = proto;
+ obj = new (proto.bind.apply(proto, args))();
+
+ if (pool) {
+ obj.className = name;
+ }
+ }
+
+ return obj;
+ }
+
+ throw new Error("Cannot instantiate object of type '" + name + "'");
+ };
+ /**
+ * purge the object pool from any inactive object
+ * Object pooling must be enabled for this function to work
+ * note: this will trigger the garbage collector
+ * @name purge
+ * @memberOf me.pool
+ * @public
+ * @function
+ */
+
+
+ api.purge = function () {
+ for (var className in objectClass) {
+ if (objectClass[className]) {
+ objectClass[className].pool = [];
+ }
+ }
+
+ instance_counter = 0;
+ };
+ /**
+ * Push back an object instance into the object pool
+ * Object pooling for the object class must be enabled,
+ * and object must have been instantiated using {@link me.pool#pull},
+ * otherwise this function won't work
+ * @name push
+ * @memberOf me.pool
+ * @public
+ * @function
+ * @param {Object} instance to be recycled
+ */
+
+
+ api.push = function (obj) {
+ var name = obj.className;
+
+ if (typeof name === "undefined" || !objectClass[name]) {
+ // object is not registered, don't do anything
+ return;
+ } // store back the object instance for later recycling
+
+
+ objectClass[name].pool.push(obj);
+ instance_counter++;
+ };
+ /**
+ * Check if an object with the provided name is registered
+ * @name exists
+ * @memberOf me.pool
+ * @public
+ * @function
+ * @param {String} name of the registered object
+ * @return {Boolean} true if the classname is registered
+ */
+
+
+ api.exists = function (name) {
+ return name in objectClass;
+ };
+ /**
+ * returns the amount of object instance currently in the pool
+ * @name getInstanceCount
+ * @memberOf me.pool
+ * @public
+ * @function
+ * @return {Number} amount of object instance
+ */
+
+
+ api.getInstanceCount = function (name) {
+ return instance_counter;
+ }; // return our object
+
+
+ return api;
+ }();
+ })();
+
+ (function () {
+ /**
+ * a collection of math utility functions
+ * @namespace Math
+ * @memberOf me
+ */
+ me.Math = function () {
+ // hold public stuff in our singleton
+ var api = {};
+ /*
+ * PUBLIC STUFF
+ */
+
+ /**
+ * constant to convert from degrees to radians
+ * @public
+ * @type {Number}
+ * @name DEG_TO_RAD
+ * @memberOf me.Math
+ */
+
+ api.DEG_TO_RAD = Math.PI / 180.0;
+ /**
+ * constant to convert from radians to degrees
+ * @public
+ * @type {Number}
+ * @name RAD_TO_DEG
+ * @memberOf me.Math
+ */
+
+ api.RAD_TO_DEG = 180.0 / Math.PI;
+ /**
+ * constant equals to 2 times pi
+ * @public
+ * @type {Number}
+ * @name TAU
+ * @memberOf me.Math
+ */
+
+ api.TAU = Math.PI * 2;
+ /**
+ * constant equals to half pi
+ * @public
+ * @type {Number}
+ * @name ETA
+ * @memberOf me.Math
+ */
+
+ api.ETA = Math.PI * 0.5;
+ /**
+ * returns true if the given value is a power of two
+ * @public
+ * @function
+ * @memberOf me.Math
+ * @name isPowerOfTwo
+ * @param {Number} val
+ * @return {boolean}
+ */
+
+ api.isPowerOfTwo = function (val) {
+ return (val & val - 1) === 0;
+ };
+ /**
+ * returns the next power of two for the given value
+ * @public
+ * @function
+ * @memberOf me.Math
+ * @name nextPowerOfTwo
+ * @param {Number} val
+ * @return {boolean}
+ */
+
+
+ api.nextPowerOfTwo = function (val) {
+ val--;
+ val |= val >> 1;
+ val |= val >> 2;
+ val |= val >> 4;
+ val |= val >> 8;
+ val |= val >> 16;
+ val++;
+ return val;
+ };
+ /**
+ * Converts an angle in degrees to an angle in radians
+ * @public
+ * @function
+ * @memberOf me.Math
+ * @name degToRad
+ * @param {number} angle angle in degrees
+ * @return {number} corresponding angle in radians
+ * @example
+ * // convert a specific angle
+ * me.Math.degToRad(60); // return 1.0471...
+ */
+
+
+ api.degToRad = function (angle) {
+ return angle * api.DEG_TO_RAD;
+ };
+ /**
+ * Converts an angle in radians to an angle in degrees.
+ * @public
+ * @function
+ * @memberOf me.Math
+ * @name radToDeg
+ * @param {number} radians angle in radians
+ * @return {number} corresponding angle in degrees
+ * @example
+ * // convert a specific angle
+ * me.Math.radToDeg(1.0471975511965976); // return 60
+ */
+
+
+ api.radToDeg = function (radians) {
+ return radians * api.RAD_TO_DEG;
+ };
+ /**
+ * clamp the given value
+ * @public
+ * @function
+ * @memberOf me.Math
+ * @name clamp
+ * @param {number} val the value to clamp
+ * @param {number} low lower limit
+ * @param {number} high higher limit
+ * @return {number} clamped value
+ */
+
+
+ api.clamp = function (val, low, high) {
+ return val < low ? low : val > high ? high : +val;
+ };
+ /**
+ * return a random integer between min (included) and max (excluded)
+ * @public
+ * @function
+ * @memberOf me.Math
+ * @name random
+ * @param {number} min minimum value.
+ * @param {number} max maximum value.
+ * @return {number} random value
+ * @example
+ * // Print a random number; one of 5, 6, 7, 8, 9
+ * console.log(me.Math.random(5, 10) );
+ */
+
+
+ api.random = function (min, max) {
+ return ~~(Math.random() * (max - min)) + min;
+ };
+ /**
+ * return a random float between min, max (exclusive)
+ * @public
+ * @function
+ * @memberOf me.Math
+ * @name randomFloat
+ * @param {number} min minimum value.
+ * @param {number} max maximum value.
+ * @return {number} random value
+ * @example
+ * // Print a random number; one of 5, 6, 7, 8, 9
+ * console.log(me.Math.randomFloat(5, 10) );
+ */
+
+
+ api.randomFloat = function (min, max) {
+ return Math.random() * (max - min) + min;
+ };
+ /**
+ * return a weighted random between min, max (exclusive)
+ * @public
+ * @function
+ * @memberOf me.Math
+ * @name weightedRandom
+ * @param {number} min minimum value.
+ * @param {number} max maximum value.
+ * @return {number} random value
+ * @example
+ * // Print a random number; one of 5, 6, 7, 8, 9
+ * console.log(me.Math.weightedRandom(5, 10) );
+ */
+
+
+ api.weightedRandom = function (min, max) {
+ return ~~(Math.pow(Math.random(), 2) * (max - min)) + min;
+ };
+ /**
+ * round a value to the specified number of digit
+ * @public
+ * @function
+ * @memberOf me.Math
+ * @name round
+ * @param {number} num value to be rounded.
+ * @param {number} [dec=0] number of decimal digit to be rounded to.
+ * @return {number} rounded value
+ * @example
+ * // round a specific value to 2 digits
+ * me.Math.round(10.33333, 2); // return 10.33
+ */
+
+
+ api.round = function (num, dec) {
+ // if only one argument use the object value
+ var powres = Math.pow(10, dec || 0);
+ return ~~(0.5 + num * powres) / powres;
+ };
+ /**
+ * check if the given value is close to the expected one
+ * @public
+ * @function
+ * @memberOf me.Math
+ * @name toBeCloseTo
+ * @param {number} expected value to be compared with.
+ * @param {number} actual actual value to compare
+ * @param {number} [precision=2] float precision for the comparison
+ * @return {boolean} if close to
+ * @example
+ * // test if the given value is close to 10
+ * if (me.Math.toBeCloseTo(10, value)) {
+ * // do something
+ * }
+ */
+
+
+ api.toBeCloseTo = function (expected, actual, precision) {
+ if (typeof precision !== "number") {
+ precision = 2;
+ }
+
+ return Math.abs(expected - actual) < Math.pow(10, -precision) / 2;
+ }; // return our object
+
+
+ return api;
+ }();
+ })();
+
+ (function () {
+ /**
+ * a generic 2D Vector Object
+ * @class
+ * @extends me.Object
+ * @memberOf me
+ * @constructor
+ * @param {Number} [x=0] x value of the vector
+ * @param {Number} [y=0] y value of the vector
+ */
+ me.Vector2d = me.Object.extend({
+ /**
+ * @ignore
+ */
+ init: function init(x, y) {
+ return this.set(x || 0, y || 0);
+ },
+
+ /**
+ * @ignore */
+ _set: function _set(x, y) {
+ this.x = x;
+ this.y = y;
+ return this;
+ },
+
+ /**
+ * set the Vector x and y properties to the given values
+ * @name set
+ * @memberOf me.Vector2d
+ * @function
+ * @param {Number} x
+ * @param {Number} y
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ set: function set(x, y) {
+ if (x !== +x || y !== +y) {
+ throw new Error("invalid x,y parameters (not a number)");
+ }
+ /**
+ * x value of the vector
+ * @public
+ * @type Number
+ * @name x
+ * @memberOf me.Vector2d
+ */
+ //this.x = x;
+
+ /**
+ * y value of the vector
+ * @public
+ * @type Number
+ * @name y
+ * @memberOf me.Vector2d
+ */
+ //this.y = y;
+
+
+ return this._set(x, y);
+ },
+
+ /**
+ * set the Vector x and y properties to 0
+ * @name setZero
+ * @memberOf me.Vector2d
+ * @function
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ setZero: function setZero() {
+ return this.set(0, 0);
+ },
+
+ /**
+ * set the Vector x and y properties using the passed vector
+ * @name setV
+ * @memberOf me.Vector2d
+ * @function
+ * @param {me.Vector2d} v
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ setV: function setV(v) {
+ return this._set(v.x, v.y);
+ },
+
+ /**
+ * Add the passed vector to this vector
+ * @name add
+ * @memberOf me.Vector2d
+ * @function
+ * @param {me.Vector2d} v
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ add: function add(v) {
+ return this._set(this.x + v.x, this.y + v.y);
+ },
+
+ /**
+ * Substract the passed vector to this vector
+ * @name sub
+ * @memberOf me.Vector2d
+ * @function
+ * @param {me.Vector2d} v
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ sub: function sub(v) {
+ return this._set(this.x - v.x, this.y - v.y);
+ },
+
+ /**
+ * Multiply this vector values by the given scalar
+ * @name scale
+ * @memberOf me.Vector2d
+ * @function
+ * @param {Number} x
+ * @param {Number} [y=x]
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ scale: function scale(x, y) {
+ return this._set(this.x * x, this.y * (typeof y !== "undefined" ? y : x));
+ },
+
+ /**
+ * Convert this vector into isometric coordinate space
+ * @name toIso
+ * @memberOf me.Vector2d
+ * @function
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ toIso: function toIso() {
+ return this._set(this.x - this.y, (this.x + this.y) * 0.5);
+ },
+
+ /**
+ * Convert this vector into 2d coordinate space
+ * @name to2d
+ * @memberOf me.Vector2d
+ * @function
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ to2d: function to2d() {
+ return this._set(this.y + this.x / 2, this.y - this.x / 2);
+ },
+
+ /**
+ * Multiply this vector values by the passed vector
+ * @name scaleV
+ * @memberOf me.Vector2d
+ * @function
+ * @param {me.Vector2d} v
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ scaleV: function scaleV(v) {
+ return this._set(this.x * v.x, this.y * v.y);
+ },
+
+ /**
+ * Divide this vector values by the passed value
+ * @name div
+ * @memberOf me.Vector2d
+ * @function
+ * @param {Number} value
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ div: function div(n) {
+ return this._set(this.x / n, this.y / n);
+ },
+
+ /**
+ * Update this vector values to absolute values
+ * @name abs
+ * @memberOf me.Vector2d
+ * @function
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ abs: function abs() {
+ return this._set(this.x < 0 ? -this.x : this.x, this.y < 0 ? -this.y : this.y);
+ },
+
+ /**
+ * Clamp the vector value within the specified value range
+ * @name clamp
+ * @memberOf me.Vector2d
+ * @function
+ * @param {Number} low
+ * @param {Number} high
+ * @return {me.Vector2d} new me.Vector2d
+ */
+ clamp: function clamp(low, high) {
+ return new me.Vector2d(me.Math.clamp(this.x, low, high), me.Math.clamp(this.y, low, high));
+ },
+
+ /**
+ * Clamp this vector value within the specified value range
+ * @name clampSelf
+ * @memberOf me.Vector2d
+ * @function
+ * @param {Number} low
+ * @param {Number} high
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ clampSelf: function clampSelf(low, high) {
+ return this._set(me.Math.clamp(this.x, low, high), me.Math.clamp(this.y, low, high));
+ },
+
+ /**
+ * Update this vector with the minimum value between this and the passed vector
+ * @name minV
+ * @memberOf me.Vector2d
+ * @function
+ * @param {me.Vector2d} v
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ minV: function minV(v) {
+ return this._set(this.x < v.x ? this.x : v.x, this.y < v.y ? this.y : v.y);
+ },
+
+ /**
+ * Update this vector with the maximum value between this and the passed vector
+ * @name maxV
+ * @memberOf me.Vector2d
+ * @function
+ * @param {me.Vector2d} v
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ maxV: function maxV(v) {
+ return this._set(this.x > v.x ? this.x : v.x, this.y > v.y ? this.y : v.y);
+ },
+
+ /**
+ * Floor the vector values
+ * @name floor
+ * @memberOf me.Vector2d
+ * @function
+ * @return {me.Vector2d} new me.Vector2d
+ */
+ floor: function floor() {
+ return new me.Vector2d(Math.floor(this.x), Math.floor(this.y));
+ },
+
+ /**
+ * Floor this vector values
+ * @name floorSelf
+ * @memberOf me.Vector2d
+ * @function
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ floorSelf: function floorSelf() {
+ return this._set(Math.floor(this.x), Math.floor(this.y));
+ },
+
+ /**
+ * Ceil the vector values
+ * @name ceil
+ * @memberOf me.Vector2d
+ * @function
+ * @return {me.Vector2d} new me.Vector2d
+ */
+ ceil: function ceil() {
+ return new me.Vector2d(Math.ceil(this.x), Math.ceil(this.y));
+ },
+
+ /**
+ * Ceil this vector values
+ * @name ceilSelf
+ * @memberOf me.Vector2d
+ * @function
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ ceilSelf: function ceilSelf() {
+ return this._set(Math.ceil(this.x), Math.ceil(this.y));
+ },
+
+ /**
+ * Negate the vector values
+ * @name negate
+ * @memberOf me.Vector2d
+ * @function
+ * @return {me.Vector2d} new me.Vector2d
+ */
+ negate: function negate() {
+ return new me.Vector2d(-this.x, -this.y);
+ },
+
+ /**
+ * Negate this vector values
+ * @name negateSelf
+ * @memberOf me.Vector2d
+ * @function
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ negateSelf: function negateSelf() {
+ return this._set(-this.x, -this.y);
+ },
+
+ /**
+ * Copy the x,y values of the passed vector to this one
+ * @name copy
+ * @memberOf me.Vector2d
+ * @function
+ * @param {me.Vector2d} v
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ copy: function copy(v) {
+ return this._set(v.x, v.y);
+ },
+
+ /**
+ * return true if the two vectors are the same
+ * @name equals
+ * @memberOf me.Vector2d
+ * @function
+ * @param {me.Vector2d} v
+ * @return {Boolean}
+ */
+ equals: function equals(v) {
+ return this.x === v.x && this.y === v.y;
+ },
+
+ /**
+ * normalize this vector (scale the vector so that its magnitude is 1)
+ * @name normalize
+ * @memberOf me.Vector2d
+ * @function
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ normalize: function normalize() {
+ var d = this.length();
+
+ if (d > 0) {
+ return this._set(this.x / d, this.y / d);
+ }
+
+ return this;
+ },
+
+ /**
+ * change this vector to be perpendicular to what it was before.
+ * (Effectively rotates it 90 degrees in a clockwise direction)
+ * @name perp
+ * @memberOf me.Vector2d
+ * @function
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ perp: function perp() {
+ return this._set(this.y, -this.x);
+ },
+
+ /**
+ * Rotate this vector (counter-clockwise) by the specified angle (in radians).
+ * @name rotate
+ * @memberOf me.Vector2d
+ * @function
+ * @param {number} angle The angle to rotate (in radians)
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ rotate: function rotate(angle) {
+ var x = this.x;
+ var y = this.y;
+ return this._set(x * Math.cos(angle) - y * Math.sin(angle), x * Math.sin(angle) + y * Math.cos(angle));
+ },
+
+ /**
+ * return the dot product of this vector and the passed one
+ * @name dotProduct
+ * @memberOf me.Vector2d
+ * @function
+ * @param {me.Vector2d} v
+ * @return {Number} The dot product.
+ */
+ dotProduct: function dotProduct(v) {
+ return this.x * v.x + this.y * v.y;
+ },
+
+ /**
+ * return the square length of this vector
+ * @name length2
+ * @memberOf me.Vector2d
+ * @function
+ * @return {Number} The length^2 of this vector.
+ */
+ length2: function length2() {
+ return this.dotProduct(this);
+ },
+
+ /**
+ * return the length (magnitude) of this vector
+ * @name length
+ * @memberOf me.Vector2d
+ * @function
+ * @return {Number} the length of this vector
+ */
+ length: function length() {
+ return Math.sqrt(this.length2());
+ },
+
+ /**
+ * Linearly interpolate between this vector and the given one.
+ * @name lerp
+ * @memberOf me.Vector2d
+ * @function
+ * @param {me.Vector2d} v
+ * @param {Number} alpha distance along the line (alpha = 0 will be this vector, and alpha = 1 will be the given one).
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ lerp: function lerp(v, alpha) {
+ this.x += (v.x - this.x) * alpha;
+ this.y += (v.y - this.y) * alpha;
+ return this;
+ },
+
+ /**
+ * return the distance between this vector and the passed one
+ * @name distance
+ * @memberOf me.Vector2d
+ * @function
+ * @param {me.Vector2d} v
+ * @return {Number}
+ */
+ distance: function distance(v) {
+ var dx = this.x - v.x,
+ dy = this.y - v.y;
+ return Math.sqrt(dx * dx + dy * dy);
+ },
+
+ /**
+ * return the angle between this vector and the passed one
+ * @name angle
+ * @memberOf me.Vector2d
+ * @function
+ * @param {me.Vector2d} v
+ * @return {Number} angle in radians
+ */
+ angle: function angle(v) {
+ return Math.acos(me.Math.clamp(this.dotProduct(v) / (this.length() * v.length()), -1, 1));
+ },
+
+ /**
+ * project this vector on to another vector.
+ * @name project
+ * @memberOf me.Vector2d
+ * @function
+ * @param {me.Vector2d} v The vector to project onto.
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ project: function project(v) {
+ return this.scale(this.dotProduct(v) / v.length2());
+ },
+
+ /**
+ * Project this vector onto a vector of unit length.
+ * This is slightly more efficient than `project` when dealing with unit vectors.
+ * @name projectN
+ * @memberOf me.Vector2d
+ * @function
+ * @param {me.Vector2d} v The unit vector to project onto.
+ * @return {me.Vector2d} Reference to this object for method chaining
+ */
+ projectN: function projectN(v) {
+ return this.scale(this.dotProduct(v));
+ },
+
+ /**
+ * return a clone copy of this vector
+ * @name clone
+ * @memberOf me.Vector2d
+ * @function
+ * @return {me.Vector2d} new me.Vector2d
+ */
+ clone: function clone() {
+ return me.pool.pull("me.Vector2d", this.x, this.y);
+ },
+
+ /**
+ * convert the object to a string representation
+ * @name toString
+ * @memberOf me.Vector2d
+ * @function
+ * @return {String}
+ */
+ toString: function toString() {
+ return "x:" + this.x + ",y:" + this.y;
+ }
+ });
+ })();
+
+ (function () {
+ /**
+ * a generic 3D Vector Object
+ * @class
+ * @extends me.Object
+ * @memberOf me
+ * @constructor
+ * @param {Number} [x=0] x value of the vector
+ * @param {Number} [y=0] y value of the vector
+ * @param {Number} [z=0] z value of the vector
+ */
+ me.Vector3d = me.Object.extend({
+ /**
+ * @ignore
+ */
+ init: function init(x, y, z) {
+ return this.set(x || 0, y || 0, z || 0);
+ },
+
+ /**
+ * @ignore */
+ _set: function _set(x, y, z) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ return this;
+ },
+
+ /**
+ * set the Vector x and y properties to the given values
+ * @name set
+ * @memberOf me.Vector3d
+ * @function
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} z
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ set: function set(x, y, z) {
+ if (x !== +x || y !== +y || z !== +z) {
+ throw new Error("invalid x, y, z parameters (not a number)");
+ }
+ /**
+ * x value of the vector
+ * @public
+ * @type Number
+ * @name x
+ * @memberOf me.Vector3d
+ */
+ //this.x = x;
+
+ /**
+ * y value of the vector
+ * @public
+ * @type Number
+ * @name y
+ * @memberOf me.Vector3d
+ */
+ //this.y = y;
+
+ /**
+ * z value of the vector
+ * @public
+ * @type Number
+ * @name z
+ * @memberOf me.Vector3d
+ */
+ //this.z = z;
+
+
+ return this._set(x, y, z);
+ },
+
+ /**
+ * set the Vector x and y properties to 0
+ * @name setZero
+ * @memberOf me.Vector3d
+ * @function
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ setZero: function setZero() {
+ return this.set(0, 0, 0);
+ },
+
+ /**
+ * set the Vector x and y properties using the passed vector
+ * @name setV
+ * @memberOf me.Vector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d} v
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ setV: function setV(v) {
+ return this._set(v.x, v.y, typeof v.z !== "undefined" ? v.z : this.z);
+ },
+
+ /**
+ * Add the passed vector to this vector
+ * @name add
+ * @memberOf me.Vector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d} v
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ add: function add(v) {
+ return this._set(this.x + v.x, this.y + v.y, this.z + (v.z || 0));
+ },
+
+ /**
+ * Substract the passed vector to this vector
+ * @name sub
+ * @memberOf me.Vector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d} v
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ sub: function sub(v) {
+ return this._set(this.x - v.x, this.y - v.y, this.z - (v.z || 0));
+ },
+
+ /**
+ * Multiply this vector values by the given scalar
+ * @name scale
+ * @memberOf me.Vector3d
+ * @function
+ * @param {Number} x
+ * @param {Number} [y=x]
+ * @param {Number} [z=x]
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ scale: function scale(x, y, z) {
+ y = typeof y !== "undefined" ? y : x;
+ z = typeof z !== "undefined" ? z : x;
+ return this._set(this.x * x, this.y * y, this.z * z);
+ },
+
+ /**
+ * Multiply this vector values by the passed vector
+ * @name scaleV
+ * @memberOf me.Vector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d} v
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ scaleV: function scaleV(v) {
+ return this._set(this.x * v.x, this.y * v.y, this.z * (v.z || 1));
+ },
+
+ /**
+ * Convert this vector into isometric coordinate space
+ * @name toIso
+ * @memberOf me.Vector3d
+ * @function
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ toIso: function toIso() {
+ return this._set(this.x - this.y, (this.x + this.y) * 0.5, this.z);
+ },
+
+ /**
+ * Convert this vector into 2d coordinate space
+ * @name to2d
+ * @memberOf me.Vector3d
+ * @function
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ to2d: function to2d() {
+ return this._set(this.y + this.x / 2, this.y - this.x / 2, this.z);
+ },
+
+ /**
+ * Divide this vector values by the passed value
+ * @name div
+ * @memberOf me.Vector3d
+ * @function
+ * @param {Number} value
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ div: function div(n) {
+ return this._set(this.x / n, this.y / n, this.z / n);
+ },
+
+ /**
+ * Update this vector values to absolute values
+ * @name abs
+ * @memberOf me.Vector3d
+ * @function
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ abs: function abs() {
+ return this._set(this.x < 0 ? -this.x : this.x, this.y < 0 ? -this.y : this.y, this.z < 0 ? -this.z : this.z);
+ },
+
+ /**
+ * Clamp the vector value within the specified value range
+ * @name clamp
+ * @memberOf me.Vector3d
+ * @function
+ * @param {Number} low
+ * @param {Number} high
+ * @return {me.Vector3d} new me.Vector3d
+ */
+ clamp: function clamp(low, high) {
+ return new me.Vector3d(me.Math.clamp(this.x, low, high), me.Math.clamp(this.y, low, high), me.Math.clamp(this.z, low, high));
+ },
+
+ /**
+ * Clamp this vector value within the specified value range
+ * @name clampSelf
+ * @memberOf me.Vector3d
+ * @function
+ * @param {Number} low
+ * @param {Number} high
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ clampSelf: function clampSelf(low, high) {
+ return this._set(me.Math.clamp(this.x, low, high), me.Math.clamp(this.y, low, high), me.Math.clamp(this.z, low, high));
+ },
+
+ /**
+ * Update this vector with the minimum value between this and the passed vector
+ * @name minV
+ * @memberOf me.Vector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d} v
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ minV: function minV(v) {
+ var _vz = v.z || 0;
+
+ return this._set(this.x < v.x ? this.x : v.x, this.y < v.y ? this.y : v.y, this.z < _vz ? this.z : _vz);
+ },
+
+ /**
+ * Update this vector with the maximum value between this and the passed vector
+ * @name maxV
+ * @memberOf me.Vector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d} v
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ maxV: function maxV(v) {
+ var _vz = v.z || 0;
+
+ return this._set(this.x > v.x ? this.x : v.x, this.y > v.y ? this.y : v.y, this.z > _vz ? this.z : _vz);
+ },
+
+ /**
+ * Floor the vector values
+ * @name floor
+ * @memberOf me.Vector3d
+ * @function
+ * @return {me.Vector3d} new me.Vector3d
+ */
+ floor: function floor() {
+ return new me.Vector3d(Math.floor(this.x), Math.floor(this.y), Math.floor(this.z));
+ },
+
+ /**
+ * Floor this vector values
+ * @name floorSelf
+ * @memberOf me.Vector3d
+ * @function
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ floorSelf: function floorSelf() {
+ return this._set(Math.floor(this.x), Math.floor(this.y), Math.floor(this.z));
+ },
+
+ /**
+ * Ceil the vector values
+ * @name ceil
+ * @memberOf me.Vector3d
+ * @function
+ * @return {me.Vector3d} new me.Vector3d
+ */
+ ceil: function ceil() {
+ return new me.Vector3d(Math.ceil(this.x), Math.ceil(this.y), Math.ceil(this.z));
+ },
+
+ /**
+ * Ceil this vector values
+ * @name ceilSelf
+ * @memberOf me.Vector3d
+ * @function
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ ceilSelf: function ceilSelf() {
+ return this._set(Math.ceil(this.x), Math.ceil(this.y), Math.ceil(this.z));
+ },
+
+ /**
+ * Negate the vector values
+ * @name negate
+ * @memberOf me.Vector3d
+ * @function
+ * @return {me.Vector3d} new me.Vector3d
+ */
+ negate: function negate() {
+ return new me.Vector3d(-this.x, -this.y, -this.z);
+ },
+
+ /**
+ * Negate this vector values
+ * @name negateSelf
+ * @memberOf me.Vector3d
+ * @function
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ negateSelf: function negateSelf() {
+ return this._set(-this.x, -this.y, -this.z);
+ },
+
+ /**
+ * Copy the x,y values of the passed vector to this one
+ * @name copy
+ * @memberOf me.Vector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d} v
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ copy: function copy(v) {
+ return this._set(v.x, v.y, typeof v.z !== "undefined" ? v.z : this.z);
+ },
+
+ /**
+ * return true if the two vectors are the same
+ * @name equals
+ * @memberOf me.Vector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d} v
+ * @return {Boolean}
+ */
+ equals: function equals(v) {
+ return this.x === v.x && this.y === v.y && this.z === (v.z || this.z);
+ },
+
+ /**
+ * normalize this vector (scale the vector so that its magnitude is 1)
+ * @name normalize
+ * @memberOf me.Vector3d
+ * @function
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ normalize: function normalize() {
+ var d = this.length();
+
+ if (d > 0) {
+ return this._set(this.x / d, this.y / d, this.z / d);
+ }
+
+ return this;
+ },
+
+ /**
+ * change this vector to be perpendicular to what it was before.
+ * (Effectively rotates it 90 degrees in a clockwise direction around the z axis)
+ * @name perp
+ * @memberOf me.Vector3d
+ * @function
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ perp: function perp() {
+ return this._set(this.y, -this.x, this.z);
+ },
+
+ /**
+ * Rotate this vector (counter-clockwise) by the specified angle (in radians) around the z axis
+ * @name rotate
+ * @memberOf me.Vector3d
+ * @function
+ * @param {number} angle The angle to rotate (in radians)
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ rotate: function rotate(angle) {
+ var x = this.x;
+ var y = this.y;
+ return this._set(x * Math.cos(angle) - y * Math.sin(angle), x * Math.sin(angle) + y * Math.cos(angle), this.z);
+ },
+
+ /**
+ * return the dot product of this vector and the passed one
+ * @name dotProduct
+ * @memberOf me.Vector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d} v
+ * @return {Number} The dot product.
+ */
+ dotProduct: function dotProduct(v) {
+ return this.x * v.x + this.y * v.y + this.z * (v.z || 1);
+ },
+
+ /**
+ * return the square length of this vector
+ * @name length2
+ * @memberOf me.Vector3d
+ * @function
+ * @return {Number} The length^2 of this vector.
+ */
+ length2: function length2() {
+ return this.dotProduct(this);
+ },
+
+ /**
+ * return the length (magnitude) of this vector
+ * @name length
+ * @memberOf me.Vector3d
+ * @function
+ * @return {Number} the length of this vector
+ */
+ length: function length() {
+ return Math.sqrt(this.length2());
+ },
+
+ /**
+ * Linearly interpolate between this vector and the given one.
+ * @name lerp
+ * @memberOf me.Vector3d
+ * @function
+ * @param {me.Vector3d} v
+ * @param {Number} alpha distance along the line (alpha = 0 will be this vector, and alpha = 1 will be the given one).
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ lerp: function lerp(v, alpha) {
+ this.x += (v.x - this.x) * alpha;
+ this.y += (v.y - this.y) * alpha;
+ this.z += (v.z - this.z) * alpha;
+ return this;
+ },
+
+ /**
+ * return the distance between this vector and the passed one
+ * @name distance
+ * @memberOf me.Vector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d} v
+ * @return {Number}
+ */
+ distance: function distance(v) {
+ var dx = this.x - v.x,
+ dy = this.y - v.y,
+ dz = this.z - (v.z || 0);
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
+ },
+
+ /**
+ * return the angle between this vector and the passed one
+ * @name angle
+ * @memberOf me.Vector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d} v
+ * @return {Number} angle in radians
+ */
+ angle: function angle(v) {
+ return Math.acos(me.Math.clamp(this.dotProduct(v) / (this.length() * v.length()), -1, 1));
+ },
+
+ /**
+ * project this vector on to another vector.
+ * @name project
+ * @memberOf me.Vector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d} v The vector to project onto.
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ project: function project(v) {
+ return this.scale(this.dotProduct(v) / v.length2());
+ },
+
+ /**
+ * Project this vector onto a vector of unit length.
+ * This is slightly more efficient than `project` when dealing with unit vectors.
+ * @name projectN
+ * @memberOf me.Vector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d} v The unit vector to project onto.
+ * @return {me.Vector3d} Reference to this object for method chaining
+ */
+ projectN: function projectN(v) {
+ return this.scale(this.dotProduct(v));
+ },
+
+ /**
+ * return a clone copy of this vector
+ * @name clone
+ * @memberOf me.Vector3d
+ * @function
+ * @return {me.Vector3d} new me.Vector3d
+ */
+ clone: function clone() {
+ return me.pool.pull("me.Vector3d", this.x, this.y, this.z);
+ },
+
+ /**
+ * convert the object to a string representation
+ * @name toString
+ * @memberOf me.Vector3d
+ * @function
+ * @return {String}
+ */
+ toString: function toString() {
+ return "x:" + this.x + ",y:" + this.y + ",z:" + this.z;
+ }
+ });
+ })();
+
+ (function () {
+ /**
+ * A Vector2d object that provide notification by executing the given callback when the vector is changed.
+ * @class
+ * @extends me.Vector2d
+ * @constructor
+ * @param {Number} [x=0] x value of the vector
+ * @param {Number} [y=0] y value of the vector
+ * @param {Object} settings additional required parameters
+ * @param {Function} settings.onUpdate the callback to be executed when the vector is changed
+ */
+ me.ObservableVector2d = me.Vector2d.extend({
+ /**
+ * @ignore
+ */
+ init: function init(x, y, settings) {
+ /**
+ * x value of the vector
+ * @public
+ * @type Number
+ * @name x
+ * @memberOf me.ObservableVector2d
+ */
+ Object.defineProperty(this, "x", {
+ /**
+ * @ignore
+ */
+ get: function get() {
+ return this._x;
+ },
+
+ /**
+ * @ignore
+ */
+ set: function set(value) {
+ var ret = this.onUpdate(value, this._y, this._x, this._y);
+
+ if (ret && "x" in ret) {
+ this._x = ret.x;
+ } else {
+ this._x = value;
+ }
+ },
+ configurable: true
+ });
+ /**
+ * y value of the vector
+ * @public
+ * @type Number
+ * @name y
+ * @memberOf me.ObservableVector2d
+ */
+
+ Object.defineProperty(this, "y", {
+ /**
+ * @ignore
+ */
+ get: function get() {
+ return this._y;
+ },
+
+ /**
+ * @ignore
+ */
+ set: function set(value) {
+ var ret = this.onUpdate(this._x, value, this._x, this._y);
+
+ if (ret && "y" in ret) {
+ this._y = ret.y;
+ } else {
+ this._y = value;
+ }
+ },
+ configurable: true
+ });
+
+ if (typeof settings === "undefined") {
+ throw new Error("undefined `onUpdate` callback");
+ }
+
+ this.setCallback(settings.onUpdate);
+ this._x = x || 0;
+ this._y = y || 0;
+ },
+
+ /** @ignore */
+ _set: function _set(x, y) {
+ var ret = this.onUpdate(x, y, this._x, this._y);
+
+ if (ret && "x" in ret && "y" in ret) {
+ this._x = ret.x;
+ this._y = ret.y;
+ } else {
+ this._x = x;
+ this._y = y;
+ }
+
+ return this;
+ },
+
+ /**
+ * set the vector value without triggering the callback
+ * @name setMuted
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @param {Number} x x value of the vector
+ * @param {Number} y y value of the vector
+ * @return {me.ObservableVector2d} Reference to this object for method chaining
+ */
+ setMuted: function setMuted(x, y) {
+ this._x = x;
+ this._y = y;
+ return this;
+ },
+
+ /**
+ * set the callback to be executed when the vector is changed
+ * @name setCallback
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @param {function} onUpdate callback
+ * @return {me.ObservableVector2d} Reference to this object for method chaining
+ */
+ setCallback: function setCallback(fn) {
+ if (typeof fn !== "function") {
+ throw new Error("invalid `onUpdate` callback");
+ }
+
+ this.onUpdate = fn;
+ return this;
+ },
+
+ /**
+ * Add the passed vector to this vector
+ * @name add
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @param {me.ObservableVector2d} v
+ * @return {me.ObservableVector2d} Reference to this object for method chaining
+ */
+ add: function add(v) {
+ return this._set(this._x + v.x, this._y + v.y);
+ },
+
+ /**
+ * Substract the passed vector to this vector
+ * @name sub
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @param {me.ObservableVector2d} v
+ * @return {me.ObservableVector2d} Reference to this object for method chaining
+ */
+ sub: function sub(v) {
+ return this._set(this._x - v.x, this._y - v.y);
+ },
+
+ /**
+ * Multiply this vector values by the given scalar
+ * @name scale
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @param {Number} x
+ * @param {Number} [y=x]
+ * @return {me.ObservableVector2d} Reference to this object for method chaining
+ */
+ scale: function scale(x, y) {
+ return this._set(this._x * x, this._y * (typeof y !== "undefined" ? y : x));
+ },
+
+ /**
+ * Multiply this vector values by the passed vector
+ * @name scaleV
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @param {me.ObservableVector2d} v
+ * @return {me.ObservableVector2d} Reference to this object for method chaining
+ */
+ scaleV: function scaleV(v) {
+ return this._set(this._x * v.x, this._y * v.y);
+ },
+
+ /**
+ * Divide this vector values by the passed value
+ * @name div
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @param {Number} value
+ * @return {me.ObservableVector2d} Reference to this object for method chaining
+ */
+ div: function div(n) {
+ return this._set(this._x / n, this._y / n);
+ },
+
+ /**
+ * Update this vector values to absolute values
+ * @name abs
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @return {me.ObservableVector2d} Reference to this object for method chaining
+ */
+ abs: function abs() {
+ return this._set(this._x < 0 ? -this._x : this._x, this._y < 0 ? -this._y : this._y);
+ },
+
+ /**
+ * Clamp the vector value within the specified value range
+ * @name clamp
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @param {Number} low
+ * @param {Number} high
+ * @return {me.ObservableVector2d} new me.ObservableVector2d
+ */
+ clamp: function clamp(low, high) {
+ return new me.ObservableVector2d(me.Math.clamp(this.x, low, high), me.Math.clamp(this.y, low, high), {
+ onUpdate: this.onUpdate
+ });
+ },
+
+ /**
+ * Clamp this vector value within the specified value range
+ * @name clampSelf
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @param {Number} low
+ * @param {Number} high
+ * @return {me.ObservableVector2d} Reference to this object for method chaining
+ */
+ clampSelf: function clampSelf(low, high) {
+ return this._set(me.Math.clamp(this._x, low, high), me.Math.clamp(this._y, low, high));
+ },
+
+ /**
+ * Update this vector with the minimum value between this and the passed vector
+ * @name minV
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @param {me.ObservableVector2d} v
+ * @return {me.ObservableVector2d} Reference to this object for method chaining
+ */
+ minV: function minV(v) {
+ return this._set(this._x < v.x ? this._x : v.x, this._y < v.y ? this._y : v.y);
+ },
+
+ /**
+ * Update this vector with the maximum value between this and the passed vector
+ * @name maxV
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @param {me.ObservableVector2d} v
+ * @return {me.ObservableVector2d} Reference to this object for method chaining
+ */
+ maxV: function maxV(v) {
+ return this._set(this._x > v.x ? this._x : v.x, this._y > v.y ? this._y : v.y);
+ },
+
+ /**
+ * Floor the vector values
+ * @name floor
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @return {me.ObservableVector2d} new me.ObservableVector2d
+ */
+ floor: function floor() {
+ return new me.ObservableVector2d(Math.floor(this._x), Math.floor(this._y), {
+ onUpdate: this.onUpdate
+ });
+ },
+
+ /**
+ * Floor this vector values
+ * @name floorSelf
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @return {me.ObservableVector2d} Reference to this object for method chaining
+ */
+ floorSelf: function floorSelf() {
+ return this._set(Math.floor(this._x), Math.floor(this._y));
+ },
+
+ /**
+ * Ceil the vector values
+ * @name ceil
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @return {me.ObservableVector2d} new me.ObservableVector2d
+ */
+ ceil: function ceil() {
+ return new me.ObservableVector2d(Math.ceil(this._x), Math.ceil(this._y), {
+ onUpdate: this.onUpdate
+ });
+ },
+
+ /**
+ * Ceil this vector values
+ * @name ceilSelf
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @return {me.ObservableVector2d} Reference to this object for method chaining
+ */
+ ceilSelf: function ceilSelf() {
+ return this._set(Math.ceil(this._x), Math.ceil(this._y));
+ },
+
+ /**
+ * Negate the vector values
+ * @name negate
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @return {me.ObservableVector2d} new me.ObservableVector2d
+ */
+ negate: function negate() {
+ return new me.ObservableVector2d(-this._x, -this._y, {
+ onUpdate: this.onUpdate
+ });
+ },
+
+ /**
+ * Negate this vector values
+ * @name negateSelf
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @return {me.ObservableVector2d} Reference to this object for method chaining
+ */
+ negateSelf: function negateSelf() {
+ return this._set(-this._x, -this._y);
+ },
+
+ /**
+ * Copy the x,y values of the passed vector to this one
+ * @name copy
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @param {me.ObservableVector2d} v
+ * @return {me.ObservableVector2d} Reference to this object for method chaining
+ */
+ copy: function copy(v) {
+ return this._set(v.x, v.y);
+ },
+
+ /**
+ * return true if the two vectors are the same
+ * @name equals
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @param {me.ObservableVector2d} v
+ * @return {Boolean}
+ */
+ equals: function equals(v) {
+ return this._x === v.x && this._y === v.y;
+ },
+
+ /**
+ * normalize this vector (scale the vector so that its magnitude is 1)
+ * @name normalize
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @return {me.ObservableVector2d} Reference to this object for method chaining
+ */
+ normalize: function normalize() {
+ var d = this.length();
+
+ if (d > 0) {
+ return this._set(this._x / d, this._y / d);
+ }
+
+ return this;
+ },
+
+ /**
+ * change this vector to be perpendicular to what it was before.
+ * (Effectively rotates it 90 degrees in a clockwise direction)
+ * @name perp
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @return {me.ObservableVector2d} Reference to this object for method chaining
+ */
+ perp: function perp() {
+ return this._set(this._y, -this._x);
+ },
+
+ /**
+ * Rotate this vector (counter-clockwise) by the specified angle (in radians).
+ * @name rotate
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @param {number} angle The angle to rotate (in radians)
+ * @return {me.ObservableVector2d} Reference to this object for method chaining
+ */
+ rotate: function rotate(angle) {
+ var x = this._x;
+ var y = this._y;
+ return this._set(x * Math.cos(angle) - y * Math.sin(angle), x * Math.sin(angle) + y * Math.cos(angle));
+ },
+
+ /**
+ * return the dot product of this vector and the passed one
+ * @name dotProduct
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @param {me.Vector2d|me.ObservableVector2d} v
+ * @return {Number} The dot product.
+ */
+ dotProduct: function dotProduct(v) {
+ return this._x * v.x + this._y * v.y;
+ },
+
+ /**
+ * Linearly interpolate between this vector and the given one.
+ * @name lerp
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @param {me.Vector2d|me.ObservableVector2d} v
+ * @param {Number} alpha distance along the line (alpha = 0 will be this vector, and alpha = 1 will be the given one).
+ * @return {me.ObservableVector2d} Reference to this object for method chaining
+ */
+ lerp: function lerp(v, alpha) {
+ this._x += (v.x - this._x) * alpha;
+ this._y += (v.y - this._y) * alpha;
+ return this;
+ },
+
+ /**
+ * return the distance between this vector and the passed one
+ * @name distance
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @param {me.ObservableVector2d} v
+ * @return {Number}
+ */
+ distance: function distance(v) {
+ return Math.sqrt((this._x - v.x) * (this._x - v.x) + (this._y - v.y) * (this._y - v.y));
+ },
+
+ /**
+ * return a clone copy of this vector
+ * @name clone
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @return {me.ObservableVector2d} new me.ObservableVector2d
+ */
+ clone: function clone() {
+ return me.pool.pull("me.ObservableVector2d", this._x, this._y, {
+ onUpdate: this.onUpdate
+ });
+ },
+
+ /**
+ * return a `me.Vector2d` copy of this `me.ObservableVector2d` object
+ * @name toVector2d
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @return {me.Vector2d} new me.Vector2d
+ */
+ toVector2d: function toVector2d() {
+ return me.pool.pull("me.Vector2d", this._x, this._y);
+ },
+
+ /**
+ * convert the object to a string representation
+ * @name toString
+ * @memberOf me.ObservableVector2d
+ * @function
+ * @return {String}
+ */
+ toString: function toString() {
+ return "x:" + this._x + ",y:" + this._y;
+ }
+ });
+ })();
+
+ (function () {
+ /**
+ * A Vector3d object that provide notification by executing the given callback when the vector is changed.
+ * @class
+ * @extends me.Vector3d
+ * @constructor
+ * @param {Number} [x=0] x value of the vector
+ * @param {Number} [y=0] y value of the vector
+ * @param {Number} [z=0] z value of the vector
+ * @param {Object} settings additional required parameters
+ * @param {Function} settings.onUpdate the callback to be executed when the vector is changed
+ */
+ me.ObservableVector3d = me.Vector3d.extend({
+ /**
+ * @ignore
+ */
+ init: function init(x, y, z, settings) {
+ /**
+ * x value of the vector
+ * @public
+ * @type Number
+ * @name x
+ * @memberOf me.ObservableVector3d
+ */
+ Object.defineProperty(this, "x", {
+ /**
+ * @ignore
+ */
+ get: function get() {
+ return this._x;
+ },
+
+ /**
+ * @ignore
+ */
+ set: function set(value) {
+ var ret = this.onUpdate(value, this._y, this._z, this._x, this._y, this._z);
+
+ if (ret && "x" in ret) {
+ this._x = ret.x;
+ } else {
+ this._x = value;
+ }
+ },
+ configurable: true
+ });
+ /**
+ * y value of the vector
+ * @public
+ * @type Number
+ * @name y
+ * @memberOf me.ObservableVector3d
+ */
+
+ Object.defineProperty(this, "y", {
+ /**
+ * @ignore
+ */
+ get: function get() {
+ return this._y;
+ },
+
+ /**
+ * @ignore
+ */
+ set: function set(value) {
+ var ret = this.onUpdate(this._x, value, this._z, this._x, this._y, this._z);
+
+ if (ret && "y" in ret) {
+ this._y = ret.y;
+ } else {
+ this._y = value;
+ }
+ },
+ configurable: true
+ });
+ /**
+ * z value of the vector
+ * @public
+ * @type Number
+ * @name z
+ * @memberOf me.ObservableVector3d
+ */
+
+ Object.defineProperty(this, "z", {
+ /**
+ * @ignore
+ */
+ get: function get() {
+ return this._z;
+ },
+
+ /**
+ * @ignore
+ */
+ set: function set(value) {
+ var ret = this.onUpdate(this._x, this._y, value, this._x, this._y, this._z);
+
+ if (ret && "z" in ret) {
+ this._z = ret.z;
+ } else {
+ this._z = value;
+ }
+ },
+ configurable: true
+ });
+
+ if (typeof settings === "undefined") {
+ throw new Error("undefined `onUpdate` callback");
+ }
+
+ this.setCallback(settings.onUpdate);
+ this._x = x || 0;
+ this._y = y || 0;
+ this._z = z || 0;
+ },
+
+ /**
+ * @ignore */
+ _set: function _set(x, y, z) {
+ var ret = this.onUpdate(x, y, z, this._x, this._y, this._z);
+
+ if (ret && "x" in ret && "y" in ret && "z" in ret) {
+ this._x = ret.x;
+ this._y = ret.y;
+ this._z = ret.z;
+ } else {
+ this._x = x;
+ this._y = y;
+ this._z = z;
+ }
+
+ return this;
+ },
+
+ /**
+ * set the vector value without triggering the callback
+ * @name setMuted
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @param {Number} x x value of the vector
+ * @param {Number} y y value of the vector
+ * @param {Number} z z value of the vector
+ * @return {me.ObservableVector3d} Reference to this object for method chaining
+ */
+ setMuted: function setMuted(x, y, z) {
+ this._x = x;
+ this._y = y;
+ this._z = z;
+ return this;
+ },
+
+ /**
+ * set the callback to be executed when the vector is changed
+ * @name setCallback
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @param {function} onUpdate callback
+ * @return {me.ObservableVector3d} Reference to this object for method chaining
+ */
+ setCallback: function setCallback(fn) {
+ if (typeof fn !== "function") {
+ throw new Error("invalid `onUpdate` callback");
+ }
+
+ this.onUpdate = fn;
+ return this;
+ },
+
+ /**
+ * Add the passed vector to this vector
+ * @name add
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d|me.ObservableVector2d|me.ObservableVector3d} v
+ * @return {me.ObservableVector3d} Reference to this object for method chaining
+ */
+ add: function add(v) {
+ return this._set(this._x + v.x, this._y + v.y, this._z + (v.z || 0));
+ },
+
+ /**
+ * Substract the passed vector to this vector
+ * @name sub
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d|me.ObservableVector2d|me.ObservableVector3d} v
+ * @return {me.ObservableVector3d} Reference to this object for method chaining
+ */
+ sub: function sub(v) {
+ return this._set(this._x - v.x, this._y - v.y, this._z - (v.z || 0));
+ },
+
+ /**
+ * Multiply this vector values by the given scalar
+ * @name scale
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @param {Number} x
+ * @param {Number} [y=x]
+ * @param {Number} [z=x]
+ * @return {me.ObservableVector3d} Reference to this object for method chaining
+ */
+ scale: function scale(x, y, z) {
+ y = typeof y !== "undefined" ? y : x;
+ z = typeof z !== "undefined" ? z : x;
+ return this._set(this._x * x, this._y * y, this._z * z);
+ },
+
+ /**
+ * Multiply this vector values by the passed vector
+ * @name scaleV
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d|me.ObservableVector2d|me.ObservableVector3d} v
+ * @return {me.ObservableVector3d} Reference to this object for method chaining
+ */
+ scaleV: function scaleV(v) {
+ return this._set(this._x * v.x, this._y * v.y, this._z * (v.z || 1));
+ },
+
+ /**
+ * Divide this vector values by the passed value
+ * @name div
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @param {Number} value
+ * @return {me.ObservableVector3d} Reference to this object for method chaining
+ */
+ div: function div(n) {
+ return this._set(this._x / n, this._y / n, this._z / n);
+ },
+
+ /**
+ * Update this vector values to absolute values
+ * @name abs
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @return {me.ObservableVector3d} Reference to this object for method chaining
+ */
+ abs: function abs() {
+ return this._set(this._x < 0 ? -this._x : this._x, this._y < 0 ? -this._y : this._y, this._Z < 0 ? -this._z : this._z);
+ },
+
+ /**
+ * Clamp the vector value within the specified value range
+ * @name clamp
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @param {Number} low
+ * @param {Number} high
+ * @return {me.ObservableVector3d} new me.ObservableVector3d
+ */
+ clamp: function clamp(low, high) {
+ return new me.ObservableVector3d(me.Math.clamp(this._x, low, high), me.Math.clamp(this._y, low, high), me.Math.clamp(this._z, low, high), {
+ onUpdate: this.onUpdate
+ });
+ },
+
+ /**
+ * Clamp this vector value within the specified value range
+ * @name clampSelf
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @param {Number} low
+ * @param {Number} high
+ * @return {me.ObservableVector3d} Reference to this object for method chaining
+ */
+ clampSelf: function clampSelf(low, high) {
+ return this._set(me.Math.clamp(this._x, low, high), me.Math.clamp(this._y, low, high), me.Math.clamp(this._z, low, high));
+ },
+
+ /**
+ * Update this vector with the minimum value between this and the passed vector
+ * @name minV
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d|me.ObservableVector2d|me.ObservableVector3d} v
+ * @return {me.ObservableVector3d} Reference to this object for method chaining
+ */
+ minV: function minV(v) {
+ var _vz = v.z || 0;
+
+ return this._set(this._x < v.x ? this._x : v.x, this._y < v.y ? this._y : v.y, this._z < _vz ? this._z : _vz);
+ },
+
+ /**
+ * Update this vector with the maximum value between this and the passed vector
+ * @name maxV
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d|me.ObservableVector2d|me.ObservableVector3d} v
+ * @return {me.ObservableVector3d} Reference to this object for method chaining
+ */
+ maxV: function maxV(v) {
+ var _vz = v.z || 0;
+
+ return this._set(this._x > v.x ? this._x : v.x, this._y > v.y ? this._y : v.y, this._z > _vz ? this._z : _vz);
+ },
+
+ /**
+ * Floor the vector values
+ * @name floor
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @return {me.ObservableVector3d} new me.ObservableVector3d
+ */
+ floor: function floor() {
+ return new me.ObservableVector3d(Math.floor(this._x), Math.floor(this._y), Math.floor(this._z), {
+ onUpdate: this.onUpdate
+ });
+ },
+
+ /**
+ * Floor this vector values
+ * @name floorSelf
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @return {me.ObservableVector3d} Reference to this object for method chaining
+ */
+ floorSelf: function floorSelf() {
+ return this._set(Math.floor(this._x), Math.floor(this._y), Math.floor(this._z));
+ },
+
+ /**
+ * Ceil the vector values
+ * @name ceil
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @return {me.ObservableVector3d} new me.ObservableVector3d
+ */
+ ceil: function ceil() {
+ return new me.ObservableVector3d(Math.ceil(this._x), Math.ceil(this._y), Math.ceil(this._z), {
+ onUpdate: this.onUpdate
+ });
+ },
+
+ /**
+ * Ceil this vector values
+ * @name ceilSelf
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @return {me.ObservableVector3d} Reference to this object for method chaining
+ */
+ ceilSelf: function ceilSelf() {
+ return this._set(Math.ceil(this._x), Math.ceil(this._y), Math.ceil(this._z));
+ },
+
+ /**
+ * Negate the vector values
+ * @name negate
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @return {me.ObservableVector3d} new me.ObservableVector3d
+ */
+ negate: function negate() {
+ return new me.ObservableVector3d(-this._x, -this._y, -this._z, {
+ onUpdate: this.onUpdate
+ });
+ },
+
+ /**
+ * Negate this vector values
+ * @name negateSelf
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @return {me.ObservableVector3d} Reference to this object for method chaining
+ */
+ negateSelf: function negateSelf() {
+ return this._set(-this._x, -this._y, -this._z);
+ },
+
+ /**
+ * Copy the x,y,z values of the passed vector to this one
+ * @name copy
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d|me.ObservableVector2d|me.ObservableVector3d} v
+ * @return {me.ObservableVector3d} Reference to this object for method chaining
+ */
+ copy: function copy(v) {
+ return this._set(v.x, v.y, typeof v.z !== "undefined" ? v.z : this._z);
+ },
+
+ /**
+ * return true if the two vectors are the same
+ * @name equals
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d|me.ObservableVector2d|me.ObservableVector3d} v
+ * @return {Boolean}
+ */
+ equals: function equals(v) {
+ return this._x === v.x && this._y === v.y && this._z === (v.z || this._z);
+ },
+
+ /**
+ * normalize this vector (scale the vector so that its magnitude is 1)
+ * @name normalize
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @return {me.ObservableVector3d} Reference to this object for method chaining
+ */
+ normalize: function normalize() {
+ var d = this.length();
+
+ if (d > 0) {
+ return this._set(this._x / d, this._y / d, this._z / d);
+ }
+
+ return this;
+ },
+
+ /**
+ * change this vector to be perpendicular to what it was before.
+ * (Effectively rotates it 90 degrees in a clockwise direction)
+ * @name perp
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @return {me.ObservableVector3d} Reference to this object for method chaining
+ */
+ perp: function perp() {
+ return this._set(this._y, -this._x, this._z);
+ },
+
+ /**
+ * Rotate this vector (counter-clockwise) by the specified angle (in radians).
+ * @name rotate
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @param {number} angle The angle to rotate (in radians)
+ * @return {me.ObservableVector3d} Reference to this object for method chaining
+ */
+ rotate: function rotate(angle) {
+ var x = this._x;
+ var y = this._y;
+ return this._set(x * Math.cos(angle) - y * Math.sin(angle), x * Math.sin(angle) + y * Math.cos(angle), this._z);
+ },
+
+ /**
+ * return the dot product of this vector and the passed one
+ * @name dotProduct
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d|me.ObservableVector2d|me.ObservableVector3d} v
+ * @return {Number} The dot product.
+ */
+ dotProduct: function dotProduct(v) {
+ return this._x * v.x + this._y * v.y + this._z * (v.z || 1);
+ },
+
+ /**
+ * Linearly interpolate between this vector and the given one.
+ * @name lerp
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @param {me.Vector3d|me.ObservableVector3d} v
+ * @param {Number} alpha distance along the line (alpha = 0 will be this vector, and alpha = 1 will be the given one).
+ * @return {me.ObservableVector3d} Reference to this object for method chaining
+ */
+ lerp: function lerp(v, alpha) {
+ this._x += (v.x - this._x) * alpha;
+ this._y += (v.y - this._y) * alpha;
+ this._z += (v.z - this._z) * alpha;
+ return this;
+ },
+
+ /**
+ * return the distance between this vector and the passed one
+ * @name distance
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @param {me.Vector2d|me.Vector3d|me.ObservableVector2d|me.ObservableVector3d} v
+ * @return {Number}
+ */
+ distance: function distance(v) {
+ var dx = this._x - v.x,
+ dy = this._y - v.y,
+ dz = this._z - (v.z || 0);
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
+ },
+
+ /**
+ * return a clone copy of this vector
+ * @name clone
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @return {me.ObservableVector3d} new me.ObservableVector3d
+ */
+ clone: function clone() {
+ return me.pool.pull("me.ObservableVector3d", this._x, this._y, this._z, {
+ onUpdate: this.onUpdate
+ });
+ },
+
+ /**
+ * return a `me.Vector3d` copy of this `me.ObservableVector3d` object
+ * @name toVector3d
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @return {me.Vector3d} new me.Vector3d
+ */
+ toVector3d: function toVector3d() {
+ return me.pool.pull("me.Vector3d", this._x, this._y, this._z);
+ },
+
+ /**
+ * convert the object to a string representation
+ * @name toString
+ * @memberOf me.ObservableVector3d
+ * @function
+ * @return {String}
+ */
+ toString: function toString() {
+ return "x:" + this._x + ",y:" + this._y + ",z:" + this._z;
+ }
+ });
+ })();
+
+ (function () {
+ /**
+ * a Matrix2d Object.
+ * the identity matrix and parameters position :
+ *
+ * @class
+ * @extends me.Object
+ * @memberOf me
+ * @constructor
+ * @param {me.Matrix2d} [mat2d] An instance of me.Matrix2d to copy from
+ * @param {Number[]} [arguments...] Matrix elements. See {@link me.Matrix2d.setTransform}
+ */
+ me.Matrix2d = me.Object.extend({
+ /**
+ * @ignore
+ */
+ init: function init() {
+ if (typeof this.val === "undefined") {
+ this.val = new Float32Array(9);
+ }
+
+ if (arguments.length && arguments[0] instanceof me.Matrix2d) {
+ this.copy(arguments[0]);
+ } else if (arguments.length >= 6) {
+ this.setTransform.apply(this, arguments);
+ } else {
+ this.identity();
+ }
+ },
+
+ /**
+ * reset the transformation matrix to the identity matrix (no transformation).
+ * the identity matrix and parameters position :
+ *
+ * @name identity
+ * @memberOf me.Matrix2d
+ * @function
+ * @return {me.Matrix2d} Reference to this object for method chaining
+ */
+ identity: function identity() {
+ this.setTransform(1, 0, 0, 0, 1, 0, 0, 0, 1);
+ return this;
+ },
+
+ /**
+ * set the matrix to the specified value
+ * @name setTransform
+ * @memberOf me.Matrix2d
+ * @function
+ * @param {Number} a
+ * @param {Number} b
+ * @param {Number} c
+ * @param {Number} d
+ * @param {Number} e
+ * @param {Number} f
+ * @param {Number} [g=0]
+ * @param {Number} [h=0]
+ * @param {Number} [i=1]
+ * @return {me.Matrix2d} Reference to this object for method chaining
+ */
+ setTransform: function setTransform() {
+ var a = this.val;
+
+ if (arguments.length === 9) {
+ a[0] = arguments[0]; // a - m00
+
+ a[1] = arguments[1]; // b - m10
+
+ a[2] = arguments[2]; // c - m20
+
+ a[3] = arguments[3]; // d - m01
+
+ a[4] = arguments[4]; // e - m11
+
+ a[5] = arguments[5]; // f - m21
+
+ a[6] = arguments[6]; // g - m02
+
+ a[7] = arguments[7]; // h - m12
+
+ a[8] = arguments[8]; // i - m22
+ } else if (arguments.length === 6) {
+ a[0] = arguments[0]; // a
+
+ a[1] = arguments[2]; // c
+
+ a[2] = arguments[4]; // e
+
+ a[3] = arguments[1]; // b
+
+ a[4] = arguments[3]; // d
+
+ a[5] = arguments[5]; // f
+
+ a[6] = 0; // g
+
+ a[7] = 0; // h
+
+ a[8] = 1; // i
+ }
+
+ return this;
+ },
+
+ /**
+ * Copies over the values from another me.Matrix2d.
+ * @name copy
+ * @memberOf me.Matrix2d
+ * @function
+ * @param {me.Matrix2d} m the matrix object to copy from
+ * @return {me.Matrix2d} Reference to this object for method chaining
+ */
+ copy: function copy(b) {
+ this.val.set(b.val);
+ return this;
+ },
+
+ /**
+ * multiply both matrix
+ * @name multiply
+ * @memberOf me.Matrix2d
+ * @function
+ * @param {me.Matrix2d} b Other matrix
+ * @return {me.Matrix2d} Reference to this object for method chaining
+ */
+ multiply: function multiply(b) {
+ b = b.val;
+ var a = this.val,
+ a0 = a[0],
+ a1 = a[1],
+ a3 = a[3],
+ a4 = a[4],
+ b0 = b[0],
+ b1 = b[1],
+ b3 = b[3],
+ b4 = b[4],
+ b6 = b[6],
+ b7 = b[7];
+ a[0] = a0 * b0 + a3 * b1;
+ a[1] = a1 * b0 + a4 * b1;
+ a[3] = a0 * b3 + a3 * b4;
+ a[4] = a1 * b3 + a4 * b4;
+ a[6] += a0 * b6 + a3 * b7;
+ a[7] += a1 * b6 + a4 * b7;
+ return this;
+ },
+
+ /**
+ * Transpose the value of this matrix.
+ * @name transpose
+ * @memberOf me.Matrix2d
+ * @function
+ * @return {me.Matrix2d} Reference to this object for method chaining
+ */
+ transpose: function transpose() {
+ var tmp,
+ a = this.val;
+ tmp = a[1];
+ a[1] = a[3];
+ a[3] = tmp;
+ tmp = a[2];
+ a[2] = a[6];
+ a[6] = tmp;
+ tmp = a[5];
+ a[5] = a[7];
+ a[7] = tmp;
+ return this;
+ },
+
+ /**
+ * invert this matrix, causing it to apply the opposite transformation.
+ * @name invert
+ * @memberOf me.Matrix2d
+ * @function
+ * @return {me.Matrix2d} Reference to this object for method chaining
+ */
+ invert: function invert() {
+ var val = this.val;
+ var a = val[0],
+ b = val[1],
+ c = val[2],
+ d = val[3],
+ e = val[4],
+ f = val[5],
+ g = val[6],
+ h = val[7],
+ i = val[8];
+ var ta = i * e - f * h,
+ td = f * g - i * d,
+ tg = h * d - e * g;
+ var n = a * ta + b * td + c * tg;
+ val[0] = ta / n;
+ val[1] = (c * h - i * b) / n;
+ val[2] = (f * b - c * e) / n;
+ val[3] = td / n;
+ val[4] = (i * a - c * g) / n;
+ val[5] = (c * d - f * a) / n;
+ val[6] = tg / n;
+ val[7] = (b * g - h * a) / n;
+ val[8] = (e * a - b * d) / n;
+ return this;
+ },
+
+ /**
+ * Transforms the given vector according to this matrix.
+ * @name multiplyVector
+ * @memberOf me.Matrix2d
+ * @function
+ * @param {me.Vector2d} vector the vector object to be transformed
+ * @return {me.Vector2d} result vector object. Useful for chaining method calls.
+ */
+ multiplyVector: function multiplyVector(v) {
+ var a = this.val,
+ x = v.x,
+ y = v.y;
+ v.x = x * a[0] + y * a[3] + a[6];
+ v.y = x * a[1] + y * a[4] + a[7];
+ return v;
+ },
+
+ /**
+ * Transforms the given vector using the inverted current matrix.
+ * @name multiplyVector
+ * @memberOf me.Matrix2d
+ * @function
+ * @param {me.Vector2d} vector the vector object to be transformed
+ * @return {me.Vector2d} result vector object. Useful for chaining method calls.
+ */
+ multiplyVectorInverse: function multiplyVectorInverse(v) {
+ var a = this.val,
+ x = v.x,
+ y = v.y;
+ var invD = 1 / (a[0] * a[4] + a[3] * -a[1]);
+ v.x = a[4] * invD * x + -a[3] * invD * y + (a[7] * a[3] - a[6] * a[4]) * invD;
+ v.y = a[0] * invD * y + -a[1] * invD * x + (-a[7] * a[0] + a[6] * a[1]) * invD;
+ return v;
+ },
+
+ /**
+ * scale the matrix
+ * @name scale
+ * @memberOf me.Matrix2d
+ * @function
+ * @param {Number} x a number representing the abscissa of the scaling vector.
+ * @param {Number} [y=x] a number representing the ordinate of the scaling vector.
+ * @return {me.Matrix2d} Reference to this object for method chaining
+ */
+ scale: function scale(x, y) {
+ var a = this.val,
+ _x = x,
+ _y = typeof y === "undefined" ? _x : y;
+
+ a[0] *= _x;
+ a[1] *= _x;
+ a[3] *= _y;
+ a[4] *= _y;
+ return this;
+ },
+
+ /**
+ * adds a 2D scaling transformation.
+ * @name scaleV
+ * @memberOf me.Matrix2d
+ * @function
+ * @param {me.Vector2d} vector scaling vector
+ * @return {me.Matrix2d} Reference to this object for method chaining
+ */
+ scaleV: function scaleV(v) {
+ return this.scale(v.x, v.y);
+ },
+
+ /**
+ * specifies a 2D scale operation using the [sx, 1] scaling vector
+ * @name scaleX
+ * @memberOf me.Matrix2d
+ * @function
+ * @param {Number} x x scaling vector
+ * @return {me.Matrix2d} Reference to this object for method chaining
+ */
+ scaleX: function scaleX(x) {
+ return this.scale(x, 1);
+ },
+
+ /**
+ * specifies a 2D scale operation using the [1,sy] scaling vector
+ * @name scaleY
+ * @memberOf me.Matrix2d
+ * @function
+ * @param {Number} y y scaling vector
+ * @return {me.Matrix2d} Reference to this object for method chaining
+ */
+ scaleY: function scaleY(y) {
+ return this.scale(1, y);
+ },
+
+ /**
+ * rotate the matrix (counter-clockwise) by the specified angle (in radians).
+ * @name rotate
+ * @memberOf me.Matrix2d
+ * @function
+ * @param {Number} angle Rotation angle in radians.
+ * @return {me.Matrix2d} Reference to this object for method chaining
+ */
+ rotate: function rotate(angle) {
+ if (angle !== 0) {
+ var a = this.val,
+ a0 = a[0],
+ a1 = a[1],
+ a3 = a[3],
+ a4 = a[4],
+ s = Math.sin(angle),
+ c = Math.cos(angle);
+ a[0] = a0 * c + a3 * s;
+ a[1] = a1 * c + a4 * s;
+ a[3] = a0 * -s + a3 * c;
+ a[4] = a1 * -s + a4 * c;
+ }
+
+ return this;
+ },
+
+ /**
+ * translate the matrix position on the horizontal and vertical axis
+ * @name translate
+ * @memberOf me.Matrix2d
+ * @function
+ * @param {Number} x the x coordindates to translate the matrix by
+ * @param {Number} y the y coordindates to translate the matrix by
+ * @return {me.Matrix2d} Reference to this object for method chaining
+ */
+ translate: function translate(x, y) {
+ var a = this.val;
+ a[6] += a[0] * x + a[3] * y;
+ a[7] += a[1] * x + a[4] * y;
+ return this;
+ },
+
+ /**
+ * translate the matrix by a vector on the horizontal and vertical axis
+ * @name translateV
+ * @memberOf me.Matrix2d
+ * @function
+ * @param {me.Vector2d} v the vector to translate the matrix by
+ * @return {me.Matrix2d} Reference to this object for method chaining
+ */
+ translateV: function translateV(v) {
+ return this.translate(v.x, v.y);
+ },
+
+ /**
+ * returns true if the matrix is an identity matrix.
+ * @name isIdentity
+ * @memberOf me.Matrix2d
+ * @function
+ * @return {Boolean}
+ **/
+ isIdentity: function isIdentity() {
+ var a = this.val;
+ 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;
+ },
+
+ /**
+ * Clone the Matrix
+ * @name clone
+ * @memberOf me.Matrix2d
+ * @function
+ * @return {me.Matrix2d}
+ */
+ clone: function clone() {
+ return me.pool.pull("me.Matrix2d", this);
+ },
+
+ /**
+ * convert the object to a string representation
+ * @name toString
+ * @memberOf me.Matrix2d
+ * @function
+ * @return {String}
+ */
+ toString: function toString() {
+ var a = this.val;
+ return "me.Matrix2d(" + a[0] + ", " + a[1] + ", " + a[2] + ", " + a[3] + ", " + a[4] + ", " + a[5] + ", " + a[6] + ", " + a[7] + ", " + a[8] + ")";
+ }
+ });
+ /**
+ * tx component of the matrix
+ * @public
+ * @type {Number}
+ * @readonly
+ * @see me.Matrix2d.translate
+ * @name tx
+ * @memberOf me.Matrix2d
+ */
+
+ Object.defineProperty(me.Matrix2d.prototype, "tx", {
+ /**
+ * @ignore
+ */
+ get: function get() {
+ return this.val[6];
+ },
+ configurable: true
+ });
+ /**
+ * ty component of the matrix
+ * @public
+ * @type {Number}
+ * @readonly
+ * @see me.Matrix2d.translate
+ * @name ty
+ * @memberOf me.Matrix2d
+ */
+
+ Object.defineProperty(me.Matrix2d.prototype, "ty", {
+ /**
+ * @ignore
+ */
+ get: function get() {
+ return this.val[7];
+ },
+ configurable: true
+ });
+ })();
+
+ (function () {
+ /**
+ * an ellipse Object
+ * @class
+ * @extends me.Object
+ * @memberOf me
+ * @constructor
+ * @param {Number} x the center x coordinate of the ellipse
+ * @param {Number} y the center y coordinate of the ellipse
+ * @param {Number} w width (diameter) of the ellipse
+ * @param {Number} h height (diameter) of the ellipse
+ */
+ me.Ellipse = me.Object.extend({
+ /**
+ * @ignore
+ */
+ init: function init(x, y, w, h) {
+ /**
+ * the center coordinates of the ellipse
+ * @public
+ * @type {me.Vector2d}
+ * @name pos
+ * @memberOf me.Ellipse#
+ */
+ this.pos = new me.Vector2d();
+ /**
+ * The bounding rectangle for this shape
+ * @private
+ * @type {me.Rect}
+ * @name _bounds
+ * @memberOf me.Ellipse#
+ */
+
+ this._bounds = undefined;
+ /**
+ * Maximum radius of the ellipse
+ * @public
+ * @type {Number}
+ * @name radius
+ * @memberOf me.Ellipse
+ */
+
+ this.radius = NaN;
+ /**
+ * Pre-scaled radius vector for ellipse
+ * @public
+ * @type {me.Vector2d}
+ * @name radiusV
+ * @memberOf me.Ellipse#
+ */
+
+ this.radiusV = new me.Vector2d();
+ /**
+ * Radius squared, for pythagorean theorom
+ * @public
+ * @type {me.Vector2d}
+ * @name radiusSq
+ * @memberOf me.Ellipse#
+ */
+
+ this.radiusSq = new me.Vector2d();
+ /**
+ * x/y scaling ratio for ellipse
+ * @public
+ * @type {me.Vector2d}
+ * @name ratio
+ * @memberOf me.Ellipse#
+ */
+
+ this.ratio = new me.Vector2d(); // the shape type
+
+ this.shapeType = "Ellipse";
+ this.setShape(x, y, w, h);
+ },
+
+ /** @ignore */
+ onResetEvent: function onResetEvent(x, y, w, h) {
+ this.setShape(x, y, w, h);
+ },
+
+ /**
+ * set new value to the Ellipse shape
+ * @name setShape
+ * @memberOf me.Ellipse.prototype
+ * @function
+ * @param {Number} x position of the ellipse
+ * @param {Number} y position of the ellipse
+ * @param {Number} w width (diameter) of the ellipse
+ * @param {Number} h height (diameter) of the ellipse
+ */
+ setShape: function setShape(x, y, w, h) {
+ var hW = w / 2;
+ var hH = h / 2;
+ this.pos.set(x, y);
+ this.radius = Math.max(hW, hH);
+ this.ratio.set(hW / this.radius, hH / this.radius);
+ this.radiusV.set(this.radius, this.radius).scaleV(this.ratio);
+ var r = this.radius * this.radius;
+ this.radiusSq.set(r, r).scaleV(this.ratio);
+ this.updateBounds();
+ return this;
+ },
+
+ /**
+ * Rotate this Ellipse (counter-clockwise) by the specified angle (in radians).
+ * @name rotate
+ * @memberOf me.Ellipse.prototype
+ * @function
+ * @param {Number} angle The angle to rotate (in radians)
+ * @return {me.Ellipse} Reference to this object for method chaining
+ */
+ rotate: function rotate()
+ /*angle*/
+ {
+ // TODO
+ return this;
+ },
+
+ /**
+ * Scale this Ellipse by the specified scalar.
+ * @name scale
+ * @memberOf me.Ellipse.prototype
+ * @function
+ * @param {Number} x
+ * @param {Number} [y=x]
+ * @return {me.Ellipse} Reference to this object for method chaining
+ */
+ scale: function scale(x, y) {
+ y = typeof y !== "undefined" ? y : x;
+ return this.setShape(this.pos.x, this.pos.y, this.radiusV.x * 2 * x, this.radiusV.y * 2 * y);
+ },
+
+ /**
+ * Scale this Ellipse by the specified vector.
+ * @name scale
+ * @memberOf me.Ellipse.prototype
+ * @function
+ * @param {me.Vector2d} v
+ * @return {me.Ellipse} Reference to this object for method chaining
+ */
+ scaleV: function scaleV(v) {
+ return this.scale(v.x, v.y);
+ },
+
+ /**
+ * apply the given transformation matrix to this ellipse
+ * @name transform
+ * @memberOf me.Ellipse.prototype
+ * @function
+ * @param {me.Matrix2d} matrix the transformation matrix
+ * @return {me.Polygon} Reference to this object for method chaining
+ */
+ transform: function transform()
+ /* m */
+ {
+ // TODO
+ return this;
+ },
+
+ /**
+ * translate the circle/ellipse by the specified offset
+ * @name translate
+ * @memberOf me.Ellipse.prototype
+ * @function
+ * @param {Number} x x offset
+ * @param {Number} y y offset
+ * @return {me.Ellipse} this ellipse
+ */
+ translate: function translate(x, y) {
+ this.pos.x += x;
+ this.pos.y += y;
+
+ this._bounds.translate(x, y);
+
+ return this;
+ },
+
+ /**
+ * translate the circle/ellipse by the specified vector
+ * @name translateV
+ * @memberOf me.Ellipse.prototype
+ * @function
+ * @param {me.Vector2d} v vector offset
+ * @return {me.Rect} this ellipse
+ */
+ translateV: function translateV(v) {
+ this.pos.add(v);
+
+ this._bounds.translateV(v);
+
+ return this;
+ },
+
+ /**
+ * check if this circle/ellipse contains the specified point
+ * @name containsPointV
+ * @memberOf me.Ellipse.prototype
+ * @function
+ * @param {me.Vector2d} point
+ * @return {boolean} true if contains
+ */
+ containsPointV: function containsPointV(v) {
+ return this.containsPoint(v.x, v.y);
+ },
+
+ /**
+ * check if this circle/ellipse contains the specified point
+ * @name containsPoint
+ * @memberOf me.Ellipse.prototype
+ * @function
+ * @param {Number} x x coordinate
+ * @param {Number} y y coordinate
+ * @return {boolean} true if contains
+ */
+ containsPoint: function containsPoint(x, y) {
+ // Make position relative to object center point.
+ x -= this.pos.x;
+ y -= this.pos.y; // Pythagorean theorem.
+
+ return x * x / this.radiusSq.x + y * y / this.radiusSq.y <= 1.0;
+ },
+
+ /**
+ * returns the bounding box for this shape, the smallest Rectangle object completely containing this shape.
+ * @name getBounds
+ * @memberOf me.Ellipse.prototype
+ * @function
+ * @return {me.Rect} this shape bounding box Rectangle object
+ */
+ getBounds: function getBounds() {
+ return this._bounds;
+ },
+
+ /**
+ * update the bounding box for this shape.
+ * @name updateBounds
+ * @memberOf me.Ellipse.prototype
+ * @function
+ * @return {me.Rect} this shape bounding box Rectangle object
+ */
+ updateBounds: function updateBounds() {
+ var rx = this.radiusV.x,
+ ry = this.radiusV.y,
+ x = this.pos.x - rx,
+ y = this.pos.y - ry,
+ w = rx * 2,
+ h = ry * 2;
+
+ if (!this._bounds) {
+ this._bounds = new me.Rect(x, y, w, h);
+ } else {
+ this._bounds.setShape(x, y, w, h);
+ }
+
+ return this._bounds;
+ },
+
+ /**
+ * clone this Ellipse
+ * @name clone
+ * @memberOf me.Ellipse.prototype
+ * @function
+ * @return {me.Ellipse} new Ellipse
+ */
+ clone: function clone() {
+ return new me.Ellipse(this.pos.x, this.pos.y, this.radiusV.x * 2, this.radiusV.y * 2);
+ }
+ });
+ })();
+
+ var earcut_1 = earcut;
+ var default_1 = earcut;
+
+ function earcut(data, holeIndices, dim) {
+
+ dim = dim || 2;
+
+ var hasHoles = holeIndices && holeIndices.length,
+ outerLen = hasHoles ? holeIndices[0] * dim : data.length,
+ outerNode = linkedList(data, 0, outerLen, dim, true),
+ triangles = [];
+
+ if (!outerNode || outerNode.next === outerNode.prev) return triangles;
+
+ var minX, minY, maxX, maxY, x, y, invSize;
+
+ if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim);
+
+ // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox
+ if (data.length > 80 * dim) {
+ minX = maxX = data[0];
+ minY = maxY = data[1];
+
+ for (var i = dim; i < outerLen; i += dim) {
+ x = data[i];
+ y = data[i + 1];
+ if (x < minX) minX = x;
+ if (y < minY) minY = y;
+ if (x > maxX) maxX = x;
+ if (y > maxY) maxY = y;
+ }
+
+ // minX, minY and invSize are later used to transform coords into integers for z-order calculation
+ invSize = Math.max(maxX - minX, maxY - minY);
+ invSize = invSize !== 0 ? 1 / invSize : 0;
+ }
+
+ earcutLinked(outerNode, triangles, dim, minX, minY, invSize);
+
+ return triangles;
+ }
+
+ // create a circular doubly linked list from polygon points in the specified winding order
+ function linkedList(data, start, end, dim, clockwise) {
+ var i, last;
+
+ if (clockwise === (signedArea(data, start, end, dim) > 0)) {
+ for (i = start; i < end; i += dim) last = insertNode(i, data[i], data[i + 1], last);
+ } else {
+ for (i = end - dim; i >= start; i -= dim) last = insertNode(i, data[i], data[i + 1], last);
+ }
+
+ if (last && equals(last, last.next)) {
+ removeNode(last);
+ last = last.next;
+ }
+
+ return last;
+ }
+
+ // eliminate colinear or duplicate points
+ function filterPoints(start, end) {
+ if (!start) return start;
+ if (!end) end = start;
+
+ var p = start,
+ again;
+ do {
+ again = false;
+
+ if (!p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) === 0)) {
+ removeNode(p);
+ p = end = p.prev;
+ if (p === p.next) break;
+ again = true;
+
+ } else {
+ p = p.next;
+ }
+ } while (again || p !== end);
+
+ return end;
+ }
+
+ // main ear slicing loop which triangulates a polygon (given as a linked list)
+ function earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) {
+ if (!ear) return;
+
+ // interlink polygon nodes in z-order
+ if (!pass && invSize) indexCurve(ear, minX, minY, invSize);
+
+ var stop = ear,
+ prev, next;
+
+ // iterate through ears, slicing them one by one
+ while (ear.prev !== ear.next) {
+ prev = ear.prev;
+ next = ear.next;
+
+ if (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear)) {
+ // cut off the triangle
+ triangles.push(prev.i / dim);
+ triangles.push(ear.i / dim);
+ triangles.push(next.i / dim);
+
+ removeNode(ear);
+
+ // skipping the next vertex leads to less sliver triangles
+ ear = next.next;
+ stop = next.next;
+
+ continue;
+ }
+
+ ear = next;
+
+ // if we looped through the whole remaining polygon and can't find any more ears
+ if (ear === stop) {
+ // try filtering points and slicing again
+ if (!pass) {
+ earcutLinked(filterPoints(ear), triangles, dim, minX, minY, invSize, 1);
+
+ // if this didn't work, try curing all small self-intersections locally
+ } else if (pass === 1) {
+ ear = cureLocalIntersections(ear, triangles, dim);
+ earcutLinked(ear, triangles, dim, minX, minY, invSize, 2);
+
+ // as a last resort, try splitting the remaining polygon into two
+ } else if (pass === 2) {
+ splitEarcut(ear, triangles, dim, minX, minY, invSize);
+ }
+
+ break;
+ }
+ }
+ }
+
+ // check whether a polygon node forms a valid ear with adjacent nodes
+ function isEar(ear) {
+ var a = ear.prev,
+ b = ear,
+ c = ear.next;
+
+ if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
+
+ // now make sure we don't have other points inside the potential ear
+ var p = ear.next.next;
+
+ while (p !== ear.prev) {
+ if (pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
+ area(p.prev, p, p.next) >= 0) return false;
+ p = p.next;
+ }
+
+ return true;
+ }
+
+ function isEarHashed(ear, minX, minY, invSize) {
+ var a = ear.prev,
+ b = ear,
+ c = ear.next;
+
+ if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
+
+ // triangle bbox; min & max are calculated like this for speed
+ var minTX = a.x < b.x ? (a.x < c.x ? a.x : c.x) : (b.x < c.x ? b.x : c.x),
+ minTY = a.y < b.y ? (a.y < c.y ? a.y : c.y) : (b.y < c.y ? b.y : c.y),
+ maxTX = a.x > b.x ? (a.x > c.x ? a.x : c.x) : (b.x > c.x ? b.x : c.x),
+ maxTY = a.y > b.y ? (a.y > c.y ? a.y : c.y) : (b.y > c.y ? b.y : c.y);
+
+ // z-order range for the current triangle bbox;
+ var minZ = zOrder(minTX, minTY, minX, minY, invSize),
+ maxZ = zOrder(maxTX, maxTY, minX, minY, invSize);
+
+ var p = ear.prevZ,
+ n = ear.nextZ;
+
+ // look for points inside the triangle in both directions
+ while (p && p.z >= minZ && n && n.z <= maxZ) {
+ if (p !== ear.prev && p !== ear.next &&
+ pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
+ area(p.prev, p, p.next) >= 0) return false;
+ p = p.prevZ;
+
+ if (n !== ear.prev && n !== ear.next &&
+ pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y) &&
+ area(n.prev, n, n.next) >= 0) return false;
+ n = n.nextZ;
+ }
+
+ // look for remaining points in decreasing z-order
+ while (p && p.z >= minZ) {
+ if (p !== ear.prev && p !== ear.next &&
+ pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
+ area(p.prev, p, p.next) >= 0) return false;
+ p = p.prevZ;
+ }
+
+ // look for remaining points in increasing z-order
+ while (n && n.z <= maxZ) {
+ if (n !== ear.prev && n !== ear.next &&
+ pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y) &&
+ area(n.prev, n, n.next) >= 0) return false;
+ n = n.nextZ;
+ }
+
+ return true;
+ }
+
+ // go through all polygon nodes and cure small local self-intersections
+ function cureLocalIntersections(start, triangles, dim) {
+ var p = start;
+ do {
+ var a = p.prev,
+ b = p.next.next;
+
+ if (!equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) {
+
+ triangles.push(a.i / dim);
+ triangles.push(p.i / dim);
+ triangles.push(b.i / dim);
+
+ // remove two nodes involved
+ removeNode(p);
+ removeNode(p.next);
+
+ p = start = b;
+ }
+ p = p.next;
+ } while (p !== start);
+
+ return p;
+ }
+
+ // try splitting polygon into two and triangulate them independently
+ function splitEarcut(start, triangles, dim, minX, minY, invSize) {
+ // look for a valid diagonal that divides the polygon into two
+ var a = start;
+ do {
+ var b = a.next.next;
+ while (b !== a.prev) {
+ if (a.i !== b.i && isValidDiagonal(a, b)) {
+ // split the polygon in two by the diagonal
+ var c = splitPolygon(a, b);
+
+ // filter colinear points around the cuts
+ a = filterPoints(a, a.next);
+ c = filterPoints(c, c.next);
+
+ // run earcut on each half
+ earcutLinked(a, triangles, dim, minX, minY, invSize);
+ earcutLinked(c, triangles, dim, minX, minY, invSize);
+ return;
+ }
+ b = b.next;
+ }
+ a = a.next;
+ } while (a !== start);
+ }
+
+ // link every hole into the outer loop, producing a single-ring polygon without holes
+ function eliminateHoles(data, holeIndices, outerNode, dim) {
+ var queue = [],
+ i, len, start, end, list;
+
+ for (i = 0, len = holeIndices.length; i < len; i++) {
+ start = holeIndices[i] * dim;
+ end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
+ list = linkedList(data, start, end, dim, false);
+ if (list === list.next) list.steiner = true;
+ queue.push(getLeftmost(list));
+ }
+
+ queue.sort(compareX);
+
+ // process holes from left to right
+ for (i = 0; i < queue.length; i++) {
+ eliminateHole(queue[i], outerNode);
+ outerNode = filterPoints(outerNode, outerNode.next);
+ }
+
+ return outerNode;
+ }
+
+ function compareX(a, b) {
+ return a.x - b.x;
+ }
+
+ // find a bridge between vertices that connects hole with an outer ring and and link it
+ function eliminateHole(hole, outerNode) {
+ outerNode = findHoleBridge(hole, outerNode);
+ if (outerNode) {
+ var b = splitPolygon(outerNode, hole);
+ filterPoints(b, b.next);
+ }
+ }
+
+ // David Eberly's algorithm for finding a bridge between hole and outer polygon
+ function findHoleBridge(hole, outerNode) {
+ var p = outerNode,
+ hx = hole.x,
+ hy = hole.y,
+ qx = -Infinity,
+ m;
+
+ // find a segment intersected by a ray from the hole's leftmost point to the left;
+ // segment's endpoint with lesser x will be potential connection point
+ do {
+ if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
+ var x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y);
+ if (x <= hx && x > qx) {
+ qx = x;
+ if (x === hx) {
+ if (hy === p.y) return p;
+ if (hy === p.next.y) return p.next;
+ }
+ m = p.x < p.next.x ? p : p.next;
+ }
+ }
+ p = p.next;
+ } while (p !== outerNode);
+
+ if (!m) return null;
+
+ if (hx === qx) return m.prev; // hole touches outer segment; pick lower endpoint
+
+ // look for points inside the triangle of hole point, segment intersection and endpoint;
+ // if there are no points found, we have a valid connection;
+ // otherwise choose the point of the minimum angle with the ray as connection point
+
+ var stop = m,
+ mx = m.x,
+ my = m.y,
+ tanMin = Infinity,
+ tan;
+
+ p = m.next;
+
+ while (p !== stop) {
+ if (hx >= p.x && p.x >= mx && hx !== p.x &&
+ pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {
+
+ tan = Math.abs(hy - p.y) / (hx - p.x); // tangential
+
+ if ((tan < tanMin || (tan === tanMin && p.x > m.x)) && locallyInside(p, hole)) {
+ m = p;
+ tanMin = tan;
+ }
+ }
+
+ p = p.next;
+ }
+
+ return m;
+ }
+
+ // interlink polygon nodes in z-order
+ function indexCurve(start, minX, minY, invSize) {
+ var p = start;
+ do {
+ if (p.z === null) p.z = zOrder(p.x, p.y, minX, minY, invSize);
+ p.prevZ = p.prev;
+ p.nextZ = p.next;
+ p = p.next;
+ } while (p !== start);
+
+ p.prevZ.nextZ = null;
+ p.prevZ = null;
+
+ sortLinked(p);
+ }
+
+ // Simon Tatham's linked list merge sort algorithm
+ // http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
+ function sortLinked(list) {
+ var i, p, q, e, tail, numMerges, pSize, qSize,
+ inSize = 1;
+
+ do {
+ p = list;
+ list = null;
+ tail = null;
+ numMerges = 0;
+
+ while (p) {
+ numMerges++;
+ q = p;
+ pSize = 0;
+ for (i = 0; i < inSize; i++) {
+ pSize++;
+ q = q.nextZ;
+ if (!q) break;
+ }
+ qSize = inSize;
+
+ while (pSize > 0 || (qSize > 0 && q)) {
+
+ if (pSize !== 0 && (qSize === 0 || !q || p.z <= q.z)) {
+ e = p;
+ p = p.nextZ;
+ pSize--;
+ } else {
+ e = q;
+ q = q.nextZ;
+ qSize--;
+ }
+
+ if (tail) tail.nextZ = e;
+ else list = e;
+
+ e.prevZ = tail;
+ tail = e;
+ }
+
+ p = q;
+ }
+
+ tail.nextZ = null;
+ inSize *= 2;
+
+ } while (numMerges > 1);
+
+ return list;
+ }
+
+ // z-order of a point given coords and inverse of the longer side of data bbox
+ function zOrder(x, y, minX, minY, invSize) {
+ // coords are transformed into non-negative 15-bit integer range
+ x = 32767 * (x - minX) * invSize;
+ y = 32767 * (y - minY) * invSize;
+
+ x = (x | (x << 8)) & 0x00FF00FF;
+ x = (x | (x << 4)) & 0x0F0F0F0F;
+ x = (x | (x << 2)) & 0x33333333;
+ x = (x | (x << 1)) & 0x55555555;
+
+ y = (y | (y << 8)) & 0x00FF00FF;
+ y = (y | (y << 4)) & 0x0F0F0F0F;
+ y = (y | (y << 2)) & 0x33333333;
+ y = (y | (y << 1)) & 0x55555555;
+
+ return x | (y << 1);
+ }
+
+ // find the leftmost node of a polygon ring
+ function getLeftmost(start) {
+ var p = start,
+ leftmost = start;
+ do {
+ if (p.x < leftmost.x || (p.x === leftmost.x && p.y < leftmost.y)) leftmost = p;
+ p = p.next;
+ } while (p !== start);
+
+ return leftmost;
+ }
+
+ // check if a point lies within a convex triangle
+ function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) {
+ return (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 &&
+ (ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 &&
+ (bx - px) * (cy - py) - (cx - px) * (by - py) >= 0;
+ }
+
+ // check if a diagonal between two polygon nodes is valid (lies in polygon interior)
+ function isValidDiagonal(a, b) {
+ return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) &&
+ locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b);
+ }
+
+ // signed area of a triangle
+ function area(p, q, r) {
+ return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
+ }
+
+ // check if two points are equal
+ function equals(p1, p2) {
+ return p1.x === p2.x && p1.y === p2.y;
+ }
+
+ // check if two segments intersect
+ function intersects(p1, q1, p2, q2) {
+ if ((equals(p1, q1) && equals(p2, q2)) ||
+ (equals(p1, q2) && equals(p2, q1))) return true;
+ return area(p1, q1, p2) > 0 !== area(p1, q1, q2) > 0 &&
+ area(p2, q2, p1) > 0 !== area(p2, q2, q1) > 0;
+ }
+
+ // check if a polygon diagonal intersects any polygon segments
+ function intersectsPolygon(a, b) {
+ var p = a;
+ do {
+ if (p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i &&
+ intersects(p, p.next, a, b)) return true;
+ p = p.next;
+ } while (p !== a);
+
+ return false;
+ }
+
+ // check if a polygon diagonal is locally inside the polygon
+ function locallyInside(a, b) {
+ return area(a.prev, a, a.next) < 0 ?
+ area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 :
+ area(a, b, a.prev) < 0 || area(a, a.next, b) < 0;
+ }
+
+ // check if the middle point of a polygon diagonal is inside the polygon
+ function middleInside(a, b) {
+ var p = a,
+ inside = false,
+ px = (a.x + b.x) / 2,
+ py = (a.y + b.y) / 2;
+ do {
+ if (((p.y > py) !== (p.next.y > py)) && p.next.y !== p.y &&
+ (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x))
+ inside = !inside;
+ p = p.next;
+ } while (p !== a);
+
+ return inside;
+ }
+
+ // link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two;
+ // if one belongs to the outer ring and another to a hole, it merges it into a single ring
+ function splitPolygon(a, b) {
+ var a2 = new Node(a.i, a.x, a.y),
+ b2 = new Node(b.i, b.x, b.y),
+ an = a.next,
+ bp = b.prev;
+
+ a.next = b;
+ b.prev = a;
+
+ a2.next = an;
+ an.prev = a2;
+
+ b2.next = a2;
+ a2.prev = b2;
+
+ bp.next = b2;
+ b2.prev = bp;
+
+ return b2;
+ }
+
+ // create a node and optionally link it with previous one (in a circular doubly linked list)
+ function insertNode(i, x, y, last) {
+ var p = new Node(i, x, y);
+
+ if (!last) {
+ p.prev = p;
+ p.next = p;
+
+ } else {
+ p.next = last.next;
+ p.prev = last;
+ last.next.prev = p;
+ last.next = p;
+ }
+ return p;
+ }
+
+ function removeNode(p) {
+ p.next.prev = p.prev;
+ p.prev.next = p.next;
+
+ if (p.prevZ) p.prevZ.nextZ = p.nextZ;
+ if (p.nextZ) p.nextZ.prevZ = p.prevZ;
+ }
+
+ function Node(i, x, y) {
+ // vertex index in coordinates array
+ this.i = i;
+
+ // vertex coordinates
+ this.x = x;
+ this.y = y;
+
+ // previous and next vertex nodes in a polygon ring
+ this.prev = null;
+ this.next = null;
+
+ // z-order curve value
+ this.z = null;
+
+ // previous and next nodes in z-order
+ this.prevZ = null;
+ this.nextZ = null;
+
+ // indicates whether this is a steiner point
+ this.steiner = false;
+ }
+
+ // return a percentage difference between the polygon area and its triangulation area;
+ // used to verify correctness of triangulation
+ earcut.deviation = function (data, holeIndices, dim, triangles) {
+ var hasHoles = holeIndices && holeIndices.length;
+ var outerLen = hasHoles ? holeIndices[0] * dim : data.length;
+
+ var polygonArea = Math.abs(signedArea(data, 0, outerLen, dim));
+ if (hasHoles) {
+ for (var i = 0, len = holeIndices.length; i < len; i++) {
+ var start = holeIndices[i] * dim;
+ var end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
+ polygonArea -= Math.abs(signedArea(data, start, end, dim));
+ }
+ }
+
+ var trianglesArea = 0;
+ for (i = 0; i < triangles.length; i += 3) {
+ var a = triangles[i] * dim;
+ var b = triangles[i + 1] * dim;
+ var c = triangles[i + 2] * dim;
+ trianglesArea += Math.abs(
+ (data[a] - data[c]) * (data[b + 1] - data[a + 1]) -
+ (data[a] - data[b]) * (data[c + 1] - data[a + 1]));
+ }
+
+ return polygonArea === 0 && trianglesArea === 0 ? 0 :
+ Math.abs((trianglesArea - polygonArea) / polygonArea);
+ };
+
+ function signedArea(data, start, end, dim) {
+ var sum = 0;
+ for (var i = start, j = end - dim; i < end; i += dim) {
+ sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]);
+ j = i;
+ }
+ return sum;
+ }
+
+ // turn a polygon in a multi-dimensional array form (e.g. as in GeoJSON) into a form Earcut accepts
+ earcut.flatten = function (data) {
+ var dim = data[0][0].length,
+ result = {vertices: [], holes: [], dimensions: dim},
+ holeIndex = 0;
+
+ for (var i = 0; i < data.length; i++) {
+ for (var j = 0; j < data[i].length; j++) {
+ for (var d = 0; d < dim; d++) result.vertices.push(data[i][j][d]);
+ }
+ if (i > 0) {
+ holeIndex += data[i - 1].length;
+ result.holes.push(holeIndex);
+ }
+ }
+ return result;
+ };
+ earcut_1.default = default_1;
+
+ // external import
+
+ (function () {
+ /**
+ * a polygon Object.
+ * Please do note that melonJS implements a simple Axis-Aligned Boxes collision algorithm, which requires all polygons used for collision to be convex with all vertices defined with clockwise winding.
+ * A polygon is convex when all line segments connecting two points in the interior do not cross any edge of the polygon
+ * (which means that all angles are less than 180 degrees), as described here below :
+ *
+ * | (0) | + * (-1) [S]--------------[E] (1) + * | (0) | + *+ * + * @ignore + * @param {Vector} line The line segment. + * @param {Vector} point The point. + * @return {number} LEFT_VORNOI_REGION (-1) if it is the left region, + * MIDDLE_VORNOI_REGION (0) if it is the middle region, + * RIGHT_VORNOI_REGION (1) if it is the right region. + */ + + + function vornoiRegion(line, point) { + var len2 = line.length2(); + var dp = point.dotProduct(line); + + if (dp < 0) { + // If the point is beyond the start of the line, it is in the + // left vornoi region. + return LEFT_VORNOI_REGION; + } else if (dp > len2) { + // If the point is beyond the end of the line, it is in the + // right vornoi region. + return RIGHT_VORNOI_REGION; + } else { + // Otherwise, it's in the middle one. + return MIDDLE_VORNOI_REGION; + } + } + /** + * A singleton for managing collision detection (and projection-based collision response) of 2D shapes.
+ * screen is filled with the specified color and slowly goes back to normal + * @name fadeOut + * @memberOf me.Camera2d + * @function + * @param {me.Color|String} color a CSS color value + * @param {Number} [duration=1000] expressed in milliseconds + * @param {Function} [onComplete] callback once effect is over + * @example + * // fade the camera to white upon dying, reload the level, and then fade out back + * me.game.viewport.fadeIn("#fff", 150, function() { + * me.audio.play("die", false); + * me.levelDirector.reloadLevel(); + * me.game.viewport.fadeOut("#fff", 150); + * }); + */ + fadeOut: function fadeOut(color, duration, onComplete) { + this._fadeOut.color = me.pool.pull("me.Color").copy(color); + this._fadeOut.tween = me.pool.pull("me.Tween", this._fadeOut.color).to({ + alpha: 0.0 + }, duration || 1000).onComplete(onComplete || null); + this._fadeOut.tween.isPersistent = true; + + this._fadeOut.tween.start(); + }, + + /** + * fadeIn effect
+ * fade to the specified color
+ * @name fadeIn
+ * @memberOf me.Camera2d
+ * @function
+ * @param {me.Color|String} color a CSS color value
+ * @param {Number} [duration=1000] expressed in milliseconds
+ * @param {Function} [onComplete] callback once effect is over
+ * @example
+ * // flash the camera to white for 75ms
+ * me.game.viewport.fadeIn("#FFFFFF", 75);
+ */
+ fadeIn: function fadeIn(color, duration, onComplete) {
+ this._fadeIn.color = me.pool.pull("me.Color").copy(color);
+ var _alpha = this._fadeIn.color.alpha;
+ this._fadeIn.color.alpha = 0.0;
+ this._fadeIn.tween = me.pool.pull("me.Tween", this._fadeIn.color).to({
+ alpha: _alpha
+ }, duration || 1000).onComplete(onComplete || null);
+ this._fadeIn.tween.isPersistent = true;
+
+ this._fadeIn.tween.start();
+ },
+
+ /**
+ * return the camera width
+ * @name getWidth
+ * @memberOf me.Camera2d
+ * @function
+ * @return {Number}
+ */
+ getWidth: function getWidth() {
+ return this.width;
+ },
+
+ /**
+ * return the camera height
+ * @name getHeight
+ * @memberOf me.Camera2d
+ * @function
+ * @return {Number}
+ */
+ getHeight: function getHeight() {
+ return this.height;
+ },
+
+ /**
+ * set the camera position around the specified object
+ * @name focusOn
+ * @memberOf me.Camera2d
+ * @function
+ * @param {me.Renderable}
+ */
+ focusOn: function focusOn(target) {
+ var bounds = target.getBounds();
+ this.moveTo(target.pos.x + bounds.pos.x + bounds.width / 2, target.pos.y + bounds.pos.y + bounds.height / 2);
+ },
+
+ /**
+ * check if the specified renderable is in the camera
+ * @name isVisible
+ * @memberOf me.Camera2d
+ * @function
+ * @param {me.Renderable} object
+ * @param {Boolean} [floating===object.floating] if visibility check should be done against screen coordinates
+ * @return {Boolean}
+ */
+ isVisible: function isVisible(obj, floating) {
+ if (floating === true || obj.floating === true) {
+ // check against screen coordinates
+ return me.video.renderer.overlaps(obj.getBounds());
+ } else {
+ // check if within the current camera
+ return obj.getBounds().overlaps(this);
+ }
+ },
+
+ /**
+ * convert the given "local" (screen) coordinates into world coordinates
+ * @name localToWorld
+ * @memberOf me.Camera2d
+ * @function
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} [v] an optional vector object where to set the
+ * converted value
+ * @return {me.Vector2d}
+ */
+ localToWorld: function localToWorld(x, y, v) {
+ // TODO memoization for one set of coords (multitouch)
+ v = v || new me.Vector2d();
+ v.set(x, y).add(this.pos).sub(me.game.world.pos);
+
+ if (!this.currentTransform.isIdentity()) {
+ this.currentTransform.multiplyVectorInverse(v);
+ }
+
+ return v;
+ },
+
+ /**
+ * convert the given world coordinates into "local" (screen) coordinates
+ * @name worldToLocal
+ * @memberOf me.Camera2d
+ * @function
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} [v] an optional vector object where to set the
+ * converted value
+ * @return {me.Vector2d}
+ */
+ worldToLocal: function worldToLocal(x, y, v) {
+ // TODO memoization for one set of coords (multitouch)
+ v = v || new me.Vector2d();
+ v.set(x, y);
+
+ if (!this.currentTransform.isIdentity()) {
+ this.currentTransform.multiplyVector(v);
+ }
+
+ return v.sub(this.pos).add(me.game.world.pos);
+ },
+
+ /**
+ * render the camera effects
+ * @ignore
+ */
+ drawFX: function drawFX(renderer) {
+ // fading effect
+ if (this._fadeIn.tween) {
+ renderer.clearColor(this._fadeIn.color); // remove the tween if over
+
+ if (this._fadeIn.color.alpha === 1.0) {
+ this._fadeIn.tween = null;
+ me.pool.push(this._fadeIn.color);
+ this._fadeIn.color = null;
+ }
+ } // flashing effect
+
+
+ if (this._fadeOut.tween) {
+ renderer.clearColor(this._fadeOut.color); // remove the tween if over
+
+ if (this._fadeOut.color.alpha === 0.0) {
+ this._fadeOut.tween = null;
+ me.pool.push(this._fadeOut.color);
+ this._fadeOut.color = null;
+ }
+ }
+ },
+
+ /**
+ * draw all object visibile in this viewport
+ * @ignore
+ */
+ draw: function draw(renderer, container) {
+ var translateX = this.pos.x + this.offset.x;
+ var translateY = this.pos.y + this.offset.y; // translate the world coordinates by default to screen coordinates
+
+ container.currentTransform.translate(-translateX, -translateY); // clip to camera bounds
+
+ renderer.clipRect(0, 0, this.width, this.height);
+ this.preDraw(renderer);
+ container.preDraw(renderer); // draw all objects,
+ // specifying the viewport as the rectangle area to redraw
+
+ container.draw(renderer, this); // draw the viewport/camera effects
+
+ this.drawFX(renderer);
+ container.postDraw(renderer);
+ this.postDraw(renderer); // translate the world coordinates by default to screen coordinates
+
+ container.currentTransform.translate(translateX, translateY);
+ }
+ });
+ })();
+
+ (function () {
+ /**
+ * a Generic Object Entity
+ * @class
+ * @extends me.Renderable
+ * @memberOf me
+ * @constructor
+ * @param {Number} x the x coordinates of the entity object
+ * @param {Number} y the y coordinates of the entity object
+ * @param {Object} settings Entity properties, to be defined through Tiled or when calling the entity constructor
+ *
+ * @param {Number} settings.width the physical width the entity takes up in game
+ * @param {Number} settings.height the physical height the entity takes up in game
+ * @param {String} [settings.name] object entity name
+ * @param {String} [settings.id] object unique IDs
+ * @param {Image|String} [settings.image] resource name of a spritesheet to use for the entity renderable component
+ * @param {Number} [settings.framewidth=settings.width] width of a single frame in the given spritesheet
+ * @param {Number} [settings.frameheight=settings.width] height of a single frame in the given spritesheet
+ * @param {String} [settings.type] object type
+ * @param {Number} [settings.collisionMask] Mask collision detection for this object
+ * @param {me.Rect[]|me.Polygon[]|me.Line[]|me.Ellipse[]} [settings.shapes] the initial list of collision shapes (usually populated through Tiled)
+ */
+ me.Entity = me.Renderable.extend({
+ /**
+ * @ignore
+ */
+ init: function init(x, y, settings) {
+ /**
+ * The array of renderable children of this entity.
+ * @ignore
+ */
+ this.children = []; // ensure mandatory properties are defined
+
+ if (typeof settings.width !== "number" || typeof settings.height !== "number") {
+ throw new Error("height and width properties are mandatory when passing settings parameters to an object entity");
+ } // call the super constructor
+
+
+ this._super(me.Renderable, "init", [x, y, settings.width, settings.height]);
+
+ if (settings.image) {
+ // set the frame size to the given entity size, if not defined in settings
+ settings.framewidth = settings.framewidth || settings.width;
+ settings.frameheight = settings.frameheight || settings.height;
+ this.renderable = new me.Sprite(0, 0, settings);
+ } // Update anchorPoint
+
+
+ if (settings.anchorPoint) {
+ this.anchorPoint.set(settings.anchorPoint.x, settings.anchorPoint.y);
+ } // set the sprite name if specified
+
+
+ if (typeof settings.name === "string") {
+ this.name = settings.name;
+ }
+ /**
+ * object type (as defined in Tiled)
+ * @public
+ * @type String
+ * @name type
+ * @memberOf me.Entity
+ */
+
+
+ this.type = settings.type || "";
+ /**
+ * object unique ID (as defined in Tiled)
+ * @public
+ * @type Number
+ * @name id
+ * @memberOf me.Entity
+ */
+
+ this.id = settings.id || "";
+ /**
+ * dead/living state of the entity
+ * default value : true
+ * @public
+ * @type Boolean
+ * @name alive
+ * @memberOf me.Entity
+ */
+
+ this.alive = true;
+ /**
+ * the entity body object
+ * @public
+ * @type me.Body
+ * @name body
+ * @memberOf me.Entity
+ */
+ // initialize the default body
+
+ var shapes = Array.isArray(settings.shapes) ? settings.shapes : [new me.Polygon(0, 0, [new me.Vector2d(0, 0), new me.Vector2d(this.width, 0), new me.Vector2d(this.width, this.height), new me.Vector2d(0, this.height)])];
+
+ if (typeof this.body !== "undefined") {
+ this.body.init(this, shapes, this.onBodyUpdate.bind(this));
+ } else {
+ this.body = new me.Body(this, shapes, this.onBodyUpdate.bind(this));
+ } // resize the entity if required
+
+
+ if (this.width === 0 && this.height === 0) {
+ this.resize(this.body.width, this.body.height);
+ } // set the collision mask if defined
+
+
+ if (typeof settings.collisionMask !== "undefined") {
+ this.body.setCollisionMask(settings.collisionMask);
+ } // set the collision mask if defined
+
+
+ if (typeof settings.collisionType !== "undefined") {
+ if (typeof me.collision.types[settings.collisionType] !== "undefined") {
+ this.body.collisionType = me.collision.types[settings.collisionType];
+ } else {
+ throw new Error("Invalid value for the collisionType property");
+ }
+ } // disable for entities
+
+
+ this.autoTransform = false; // enable collision detection
+
+ this.isKinematic = false;
+ },
+
+ /**
+ * return the distance to the specified entity
+ * @name distanceTo
+ * @memberOf me.Entity
+ * @function
+ * @param {me.Entity} entity Entity
+ * @return {Number} distance
+ */
+ distanceTo: function distanceTo(e) {
+ var a = this.getBounds();
+ var b = e.getBounds(); // the me.Vector2d object also implements the same function, but
+ // we have to use here the center of both entities
+
+ var dx = a.pos.x + a.width / 2 - (b.pos.x + b.width / 2);
+ var dy = a.pos.y + a.height / 2 - (b.pos.y + b.height / 2);
+ return Math.sqrt(dx * dx + dy * dy);
+ },
+
+ /**
+ * return the distance to the specified point
+ * @name distanceToPoint
+ * @memberOf me.Entity
+ * @function
+ * @param {me.Vector2d} vector vector
+ * @return {Number} distance
+ */
+ distanceToPoint: function distanceToPoint(v) {
+ var a = this.getBounds(); // the me.Vector2d object also implements the same function, but
+ // we have to use here the center of both entities
+
+ var dx = a.pos.x + a.width / 2 - v.x;
+ var dy = a.pos.y + a.height / 2 - v.y;
+ return Math.sqrt(dx * dx + dy * dy);
+ },
+
+ /**
+ * return the angle to the specified entity
+ * @name angleTo
+ * @memberOf me.Entity
+ * @function
+ * @param {me.Entity} entity Entity
+ * @return {Number} angle in radians
+ */
+ angleTo: function angleTo(e) {
+ var a = this.getBounds();
+ var b = e.getBounds(); // the me.Vector2d object also implements the same function, but
+ // we have to use here the center of both entities
+
+ var ax = b.pos.x + b.width / 2 - (a.pos.x + a.width / 2);
+ var ay = b.pos.y + b.height / 2 - (a.pos.y + a.height / 2);
+ return Math.atan2(ay, ax);
+ },
+
+ /**
+ * return the angle to the specified point
+ * @name angleToPoint
+ * @memberOf me.Entity
+ * @function
+ * @param {me.Vector2d} vector vector
+ * @return {Number} angle in radians
+ */
+ angleToPoint: function angleToPoint(v) {
+ var a = this.getBounds(); // the me.Vector2d object also implements the same function, but
+ // we have to use here the center of both entities
+
+ var ax = v.x - (a.pos.x + a.width / 2);
+ var ay = v.y - (a.pos.y + a.height / 2);
+ return Math.atan2(ay, ax);
+ },
+
+ /** @ignore */
+ update: function update(dt) {
+ if (this.renderable) {
+ return this.renderable.update(dt);
+ }
+
+ return this._super(me.Renderable, "update", [dt]);
+ },
+
+ /**
+ * update the bounds position when the position is modified
+ * @private
+ * @name updateBoundsPos
+ * @memberOf me.Entity
+ * @function
+ */
+ updateBoundsPos: function updateBoundsPos(x, y) {
+ if (typeof this.body !== "undefined") {
+ var _pos = this.body.pos;
+
+ this._super(me.Renderable, "updateBoundsPos", [x + _pos.x, y + _pos.y]);
+ } else {
+ this._super(me.Renderable, "updateBoundsPos", [x, y]);
+ }
+
+ return this.getBounds();
+ },
+
+ /**
+ * update the bounds position when the body is modified
+ * @private
+ * @name onBodyUpdate
+ * @memberOf me.Entity
+ * @function
+ */
+ onBodyUpdate: function onBodyUpdate(body) {
+ // update the entity bounds to match with the body bounds
+ this.getBounds().resize(body.width, body.height); // update the bounds pos
+
+ this.updateBoundsPos(this.pos.x, this.pos.y);
+ },
+ preDraw: function preDraw(renderer) {
+ renderer.save(); // translate to the entity position
+
+ renderer.translate(this.pos.x + this.body.pos.x, this.pos.y + this.body.pos.y);
+
+ if (this.renderable instanceof me.Renderable) {
+ // draw the child renderable's anchorPoint at the entity's
+ // anchor point. the entity's anchor point is a scale from
+ // body position to body width/height
+ renderer.translate(this.anchorPoint.x * this.body.width, this.anchorPoint.y * this.body.height);
+ }
+ },
+
+ /**
+ * object draw
+ * not to be called by the end user
+ * called by the game manager on each game loop
+ * @name draw
+ * @memberOf me.Entity
+ * @function
+ * @protected
+ * @param {me.CanvasRenderer|me.WebGLRenderer} renderer a renderer object
+ * @param {me.Rect} region to draw
+ **/
+ draw: function draw(renderer, rect) {
+ var renderable = this.renderable;
+
+ if (renderable instanceof me.Renderable) {
+ // predraw (apply transforms)
+ renderable.preDraw(renderer); // draw the object
+
+ renderable.draw(renderer, rect); // postdraw (clean-up);
+
+ renderable.postDraw(renderer);
+ }
+ },
+
+ /**
+ * Destroy function
+ * @ignore
+ */
+ destroy: function destroy() {
+ // free some property objects
+ if (this.renderable) {
+ this.renderable.destroy.apply(this.renderable, arguments);
+ this.children.splice(0, 1);
+ } // call the parent destroy method
+
+
+ this._super(me.Renderable, "destroy", arguments);
+ },
+
+ /**
+ * onDeactivateEvent Notification function
+ * Called by engine before deleting the object
+ * @name onDeactivateEvent
+ * @memberOf me.Entity
+ * @function
+ */
+ onDeactivateEvent: function onDeactivateEvent() {
+ if (this.renderable && this.renderable.onDeactivateEvent) {
+ this.renderable.onDeactivateEvent();
+ }
+ },
+
+ /**
+ * onCollision callback
+ * triggered in case of collision, when this entity body is being "touched" by another one
+ * @name onCollision
+ * @memberOf me.Entity
+ * @function
+ * @param {me.collision.ResponseObject} response the collision response object
+ * @param {me.Entity} other the other entity touching this one (a reference to response.a or response.b)
+ * @return {Boolean} true if the object should respond to the collision (its position and velocity will be corrected)
+ */
+ onCollision: function onCollision() {
+ return false;
+ }
+ });
+ /**
+ * The entity renderable component (can be any objects deriving from me.Renderable, like me.Sprite for example)
+ * @public
+ * @type me.Renderable
+ * @name renderable
+ * @memberOf me.Entity
+ */
+
+ Object.defineProperty(me.Entity.prototype, "renderable", {
+ /* for backward compatiblity */
+
+ /**
+ * @ignore
+ */
+ get: function get() {
+ return this.children[0];
+ },
+
+ /**
+ * @ignore
+ */
+ set: function set(value) {
+ if (value instanceof me.Renderable) {
+ this.children[0] = value;
+ this.children[0].ancestor = this;
+ } else {
+ throw new Error(value + "should extend me.Renderable");
+ }
+ },
+ configurable: true
+ });
+ })();
+
+ (function () {
+ // a default camera instance to use across all stages
+ var default_camera; // default stage settings
+
+ var default_settings = {
+ cameras: []
+ };
+ /**
+ * A default "Stage" object
+ * every "stage" object (title screen, credits, ingame, etc...) to be managed
+ * through the state manager must inherit from this base class.
+ * @class
+ * @extends me.Object
+ * @memberOf me
+ * @constructor
+ * @param {Object} [options] The stage` parameters
+ * @param {Boolean} [options.cameras=[new me.Camera2d()]] a list of cameras (experimental)
+ * @see me.state
+ */
+
+ me.Stage = me.Object.extend({
+ /**
+ * @ignore
+ */
+ init: function init(settings) {
+ /**
+ * The list of active cameras in this stage.
+ * Cameras will be renderered based on this order defined in this list.
+ * Only the "default" camera will be resized when the window or canvas is resized.
+ * @public
+ * @type {Map}
+ * @name cameras
+ * @memberOf me.Stage
+ */
+ this.cameras = new Map();
+ /**
+ * The given constructor options
+ * @public
+ * @name settings
+ * @memberOf me.Stage
+ * @enum {Object}
+ */
+
+ this.settings = Object.assign(default_settings, settings || {});
+ },
+
+ /**
+ * Object reset function
+ * @ignore
+ */
+ reset: function reset() {
+ var self = this; // add all defined cameras
+
+ this.settings.cameras.forEach(function (camera) {
+ self.cameras.set(camera.name, camera);
+ }); // empty or no default camera
+
+ if (this.cameras.has("default") === false) {
+ if (typeof default_camera === "undefined") {
+ var width = me.video.renderer.getWidth();
+ var height = me.video.renderer.getHeight(); // new default camera instance
+
+ default_camera = new me.Camera2d(0, 0, width, height);
+ }
+
+ this.cameras.set("default", default_camera);
+ } // reset the game manager
+
+
+ me.game.reset(); // call the onReset Function
+
+ this.onResetEvent.apply(this, arguments);
+ },
+
+ /**
+ * destroy function
+ * @ignore
+ */
+ destroy: function destroy() {
+ // clear all cameras
+ this.cameras.clear(); // notify the object
+
+ this.onDestroyEvent.apply(this, arguments);
+ },
+
+ /**
+ * onResetEvent function
+ * called by the state manager when reseting the object
+ * this is typically where you will load a level, add renderables, etc...
+ * @name onResetEvent
+ * @memberOf me.Stage
+ * @function
+ * @param {} [arguments...] optional arguments passed when switching state
+ * @see me.state#change
+ */
+ onResetEvent: function onResetEvent() {// to be extended
+ },
+
+ /**
+ * onDestroyEvent function
+ * called by the state manager before switching to another state
+ * @name onDestroyEvent
+ * @memberOf me.Stage
+ * @function
+ */
+ onDestroyEvent: function onDestroyEvent() {// to be extended
+ }
+ });
+ })();
+
+ (function () {
+ /**
+ * a State Manager (state machine)
+ * There is no constructor function for me.state.
+ * @namespace me.state
+ * @memberOf me
+ */
+ me.state = function () {
+ // hold public stuff in our singleton
+ var api = {};
+ /*-------------------------------------------
+ PRIVATE STUFF
+ --------------------------------------------*/
+ // current state
+
+ var _state = -1; // requestAnimeFrame Id
+
+
+ var _animFrameId = -1; // whether the game state is "paused"
+
+
+ var _isPaused = false; // list of stages
+
+ var _stages = {}; // fading transition parameters between screen
+
+ var _fade = {
+ color: "",
+ duration: 0
+ }; // callback when state switch is done
+
+ /** @ignore */
+
+ var _onSwitchComplete = null; // just to keep track of possible extra arguments
+
+ var _extraArgs = null; // store the elapsed time during pause/stop period
+
+ var _pauseTime = 0;
+ /**
+ * @ignore
+ */
+
+ function _startRunLoop() {
+ // ensure nothing is running first and in valid state
+ if (_animFrameId === -1 && _state !== -1) {
+ // reset the timer
+ me.timer.reset(); // start the main loop
+
+ _animFrameId = window.requestAnimationFrame(_renderFrame);
+ }
+ }
+ /**
+ * Resume the game loop after a pause.
+ * @ignore
+ */
+
+
+ function _resumeRunLoop() {
+ // ensure game is actually paused and in valid state
+ if (_isPaused && _state !== -1) {
+ // reset the timer
+ me.timer.reset();
+ _isPaused = false;
+ }
+ }
+ /**
+ * Pause the loop for most screen objects.
+ * @ignore
+ */
+
+
+ function _pauseRunLoop() {
+ // Set the paused boolean to stop updates on (most) entities
+ _isPaused = true;
+ }
+ /**
+ * this is only called when using requestAnimFrame stuff
+ * @param {Number} time current timestamp in milliseconds
+ * @ignore
+ */
+
+
+ function _renderFrame(time) {
+ var stage = _stages[_state].stage; // update all game objects
+
+ me.game.update(time, stage); // render all game objects
+
+ me.game.draw(stage); // schedule the next frame update
+
+ if (_animFrameId !== -1) {
+ _animFrameId = window.requestAnimationFrame(_renderFrame);
+ }
+ }
+ /**
+ * stop the SO main loop
+ * @ignore
+ */
+
+
+ function _stopRunLoop() {
+ // cancel any previous animationRequestFrame
+ window.cancelAnimationFrame(_animFrameId);
+ _animFrameId = -1;
+ }
+ /**
+ * start the SO main loop
+ * @ignore
+ */
+
+
+ function _switchState(state) {
+ // clear previous interval if any
+ _stopRunLoop(); // call the stage destroy method
+
+
+ if (_stages[_state]) {
+ // just notify the object
+ _stages[_state].stage.destroy();
+ }
+
+ if (_stages[state]) {
+ // set the global variable
+ _state = state; // call the reset function with _extraArgs as arguments
+
+ _stages[_state].stage.reset.apply(_stages[_state].stage, _extraArgs); // and start the main loop of the
+ // new requested state
+
+
+ _startRunLoop(); // execute callback if defined
+
+
+ if (_onSwitchComplete) {
+ _onSwitchComplete();
+ } // force repaint
+
+
+ me.game.repaint();
+ }
+ }
+ /*
+ * PUBLIC STUFF
+ */
+
+ /**
+ * default state ID for Loading Screen
+ * @constant
+ * @name LOADING
+ * @memberOf me.state
+ */
+
+
+ api.LOADING = 0;
+ /**
+ * default state ID for Menu Screen
+ * @constant
+ * @name MENU
+ * @memberOf me.state
+ */
+
+ api.MENU = 1;
+ /**
+ * default state ID for "Ready" Screen
+ * @constant
+ * @name READY
+ * @memberOf me.state
+ */
+
+ api.READY = 2;
+ /**
+ * default state ID for Play Screen
+ * @constant
+ * @name PLAY
+ * @memberOf me.state
+ */
+
+ api.PLAY = 3;
+ /**
+ * default state ID for Game Over Screen
+ * @constant
+ * @name GAMEOVER
+ * @memberOf me.state
+ */
+
+ api.GAMEOVER = 4;
+ /**
+ * default state ID for Game End Screen
+ * @constant
+ * @name GAME_END
+ * @memberOf me.state
+ */
+
+ api.GAME_END = 5;
+ /**
+ * default state ID for High Score Screen
+ * @constant
+ * @name SCORE
+ * @memberOf me.state
+ */
+
+ api.SCORE = 6;
+ /**
+ * default state ID for Credits Screen
+ * @constant
+ * @name CREDITS
+ * @memberOf me.state
+ */
+
+ api.CREDITS = 7;
+ /**
+ * default state ID for Settings Screen
+ * @constant
+ * @name SETTINGS
+ * @memberOf me.state
+ */
+
+ api.SETTINGS = 8;
+ /**
+ * default state ID for user defined constants
+ * Super simple, fast and easy to use tweening engine which incorporates optimised Robert Penner's equation
+ * https://github.com/sole/Tween.js
+ * author sole / http://soledadpenades.com
+ * me.Tween.Easing.Linear.None
+ * me.Tween.Interpolation.Linear u&&(a.aInB=!1);var E=c(m,v),A=!0;if(E===e){var S=null;if(p>1&&(m.copy(f[w]),(E=c(m,S=n.pop().copy(h).sub(d[w])))!==i&&(A=!1)),A){if((y=v.length())>l)return n.push(h),n.push(m),n.push(g),n.push(v),S&&n.push(S),!1;a&&(a.bInA=!1,T=v.normalize(),b=l-y)}S&&n.push(S)}else if(E===i){if(p>1&&(m.copy(f[x]),v.copy(h).sub(d[x]),(E=c(m,v))!==e&&(A=!1)),A){if((y=v.length())>l)return n.push(h),n.push(m),n.push(g),n.push(v),!1;a&&(a.bInA=!1,T=v.normalize(),b=l-y)}}else{g.copy(r.normals[_]),y=v.dotProduct(g);var C=Math.abs(y);if((1===p||y>0)&&C>l)return n.push(h),n.push(m),n.push(g),n.push(v),!1;a&&(T=g,b=l-y,(y>=0||b<2*l)&&(a.bInA=!1))}T&&a&&Math.abs(b)
+ * @constant
+ * @name USER
+ * @memberOf me.state
+ * @example
+ * var STATE_INFO = me.state.USER + 0;
+ * var STATE_WARN = me.state.USER + 1;
+ * var STATE_ERROR = me.state.USER + 2;
+ * var STATE_CUTSCENE = me.state.USER + 3;
+ */
+
+ api.USER = 100;
+ /**
+ * onPause callback
+ * @function
+ * @name onPause
+ * @memberOf me.state
+ */
+
+ api.onPause = null;
+ /**
+ * onResume callback
+ * @function
+ * @name onResume
+ * @memberOf me.state
+ */
+
+ api.onResume = null;
+ /**
+ * onStop callback
+ * @function
+ * @name onStop
+ * @memberOf me.state
+ */
+
+ api.onStop = null;
+ /**
+ * onRestart callback
+ * @function
+ * @name onRestart
+ * @memberOf me.state
+ */
+
+ api.onRestart = null;
+ /**
+ * @ignore
+ */
+
+ api.init = function () {
+ // set the embedded loading screen
+ api.set(api.LOADING, new me.DefaultLoadingScreen());
+ };
+ /**
+ * Stop the current screen object.
+ * @name stop
+ * @memberOf me.state
+ * @public
+ * @function
+ * @param {Boolean} pauseTrack pause current track on screen stop.
+ */
+
+
+ api.stop = function (music) {
+ // only stop when we are not loading stuff
+ if (_state !== api.LOADING && api.isRunning()) {
+ // stop the main loop
+ _stopRunLoop(); // current music stop
+
+
+ if (music === true) {
+ me.audio.pauseTrack();
+ } // store time when stopped
+
+
+ _pauseTime = window.performance.now(); // publish the stop notification
+
+ me.event.publish(me.event.STATE_STOP); // any callback defined ?
+
+ if (typeof api.onStop === "function") {
+ api.onStop();
+ }
+ }
+ };
+ /**
+ * pause the current screen object
+ * @name pause
+ * @memberOf me.state
+ * @public
+ * @function
+ * @param {Boolean} pauseTrack pause current track on screen pause
+ */
+
+
+ api.pause = function (music) {
+ // only pause when we are not loading stuff
+ if (_state !== api.LOADING && !api.isPaused()) {
+ // stop the main loop
+ _pauseRunLoop(); // current music stop
+
+
+ if (music === true) {
+ me.audio.pauseTrack();
+ } // store time when paused
+
+
+ _pauseTime = window.performance.now(); // publish the pause event
+
+ me.event.publish(me.event.STATE_PAUSE); // any callback defined ?
+
+ if (typeof api.onPause === "function") {
+ api.onPause();
+ }
+ }
+ };
+ /**
+ * Restart the screen object from a full stop.
+ * @name restart
+ * @memberOf me.state
+ * @public
+ * @function
+ * @param {Boolean} resumeTrack resume current track on screen resume
+ */
+
+
+ api.restart = function (music) {
+ if (!api.isRunning()) {
+ // restart the main loop
+ _startRunLoop(); // current music stop
+
+
+ if (music === true) {
+ me.audio.resumeTrack();
+ } // calculate the elpased time
+
+
+ _pauseTime = window.performance.now() - _pauseTime; // force repaint
+
+ me.game.repaint(); // publish the restart notification
+
+ me.event.publish(me.event.STATE_RESTART, [_pauseTime]); // any callback defined ?
+
+ if (typeof api.onRestart === "function") {
+ api.onRestart();
+ }
+ }
+ };
+ /**
+ * resume the screen object
+ * @name resume
+ * @memberOf me.state
+ * @public
+ * @function
+ * @param {Boolean} resumeTrack resume current track on screen resume
+ */
+
+
+ api.resume = function (music) {
+ if (api.isPaused()) {
+ // resume the main loop
+ _resumeRunLoop(); // current music stop
+
+
+ if (music === true) {
+ me.audio.resumeTrack();
+ } // calculate the elpased time
+
+
+ _pauseTime = window.performance.now() - _pauseTime; // publish the resume event
+
+ me.event.publish(me.event.STATE_RESUME, [_pauseTime]); // any callback defined ?
+
+ if (typeof api.onResume === "function") {
+ api.onResume();
+ }
+ }
+ };
+ /**
+ * return the running state of the state manager
+ * @name isRunning
+ * @memberOf me.state
+ * @public
+ * @function
+ * @return {Boolean} true if a "process is running"
+ */
+
+
+ api.isRunning = function () {
+ return _animFrameId !== -1;
+ };
+ /**
+ * Return the pause state of the state manager
+ * @name isPaused
+ * @memberOf me.state
+ * @public
+ * @function
+ * @return {Boolean} true if the game is paused
+ */
+
+
+ api.isPaused = function () {
+ return _isPaused;
+ };
+ /**
+ * associate the specified state with a Stage
+ * @name set
+ * @memberOf me.state
+ * @public
+ * @function
+ * @param {Number} state State ID (see constants)
+ * @param {me.Stage} stage Instantiated Stage to associate
+ * with state ID
+ * @example
+ * var MenuButton = me.GUI_Object.extend({
+ * "onClick" : function () {
+ * // Change to the PLAY state when the button is clicked
+ * me.state.change(me.state.PLAY);
+ * return true;
+ * }
+ * });
+ *
+ * var MenuScreen = me.Stage.extend({
+ * onResetEvent: function() {
+ * // Load background image
+ * me.game.world.addChild(
+ * new me.ImageLayer(0, 0, {
+ * image : "bg",
+ * z: 0 // z-index
+ * }
+ * );
+ *
+ * // Add a button
+ * me.game.world.addChild(
+ * new MenuButton(350, 200, { "image" : "start" }),
+ * 1 // z-index
+ * );
+ *
+ * // Play music
+ * me.audio.playTrack("menu");
+ * },
+ *
+ * "onDestroyEvent" : function () {
+ * // Stop music
+ * me.audio.stopTrack();
+ * }
+ * });
+ *
+ * me.state.set(me.state.MENU, new MenuScreen());
+ */
+
+
+ api.set = function (state, stage) {
+ if (!(stage instanceof me.Stage)) {
+ throw new Error(stage + " is not an instance of me.Stage");
+ }
+
+ _stages[state] = {};
+ _stages[state].stage = stage;
+ _stages[state].transition = true;
+ };
+ /**
+ * return a reference to the current screen object
+ * useful to call a object specific method
+ * @name current
+ * @memberOf me.state
+ * @public
+ * @function
+ * @return {me.Stage}
+ */
+
+
+ api.current = function () {
+ return _stages[_state].stage;
+ };
+ /**
+ * specify a global transition effect
+ * @name transition
+ * @memberOf me.state
+ * @public
+ * @function
+ * @param {String} effect (only "fade" is supported for now)
+ * @param {me.Color|String} color a CSS color value
+ * @param {Number} [duration=1000] expressed in milliseconds
+ */
+
+
+ api.transition = function (effect, color, duration) {
+ if (effect === "fade") {
+ _fade.color = color;
+ _fade.duration = duration;
+ }
+ };
+ /**
+ * enable/disable transition for a specific state (by default enabled for all)
+ * @name setTransition
+ * @memberOf me.state
+ * @public
+ * @function
+ * @param {Number} state State ID (see constants)
+ * @param {Boolean} enable
+ */
+
+
+ api.setTransition = function (state, enable) {
+ _stages[state].transition = enable;
+ };
+ /**
+ * change the game/app state
+ * @name change
+ * @memberOf me.state
+ * @public
+ * @function
+ * @param {Number} state State ID (see constants)
+ * @param {} [arguments...] extra arguments to be passed to the reset functions
+ * @example
+ * // The onResetEvent method on the play screen will receive two args:
+ * // "level_1" and the number 3
+ * me.state.change(me.state.PLAY, "level_1", 3);
+ */
+
+
+ api.change = function (state) {
+ // Protect against undefined Stage
+ if (typeof _stages[state] === "undefined") {
+ throw new Error("Undefined Stage for state '" + state + "'");
+ }
+
+ if (api.isCurrent(state)) {
+ // do nothing if already the current state
+ return;
+ }
+
+ _extraArgs = null;
+
+ if (arguments.length > 1) {
+ // store extra arguments if any
+ _extraArgs = Array.prototype.slice.call(arguments, 1);
+ } // if fading effect
+
+
+ if (_fade.duration && _stages[state].transition) {
+ /** @ignore */
+ _onSwitchComplete = function _onSwitchComplete() {
+ me.game.viewport.fadeOut(_fade.color, _fade.duration);
+ };
+
+ me.game.viewport.fadeIn(_fade.color, _fade.duration, function () {
+ me.utils.function.defer(_switchState, this, state);
+ });
+ } // else just switch without any effects
+ else {
+ // wait for the last frame to be
+ // "finished" before switching
+ me.utils.function.defer(_switchState, this, state);
+ }
+ };
+ /**
+ * return true if the specified state is the current one
+ * @name isCurrent
+ * @memberOf me.state
+ * @public
+ * @function
+ * @param {Number} state State ID (see constants)
+ */
+
+
+ api.isCurrent = function (state) {
+ return _state === state;
+ }; // return our object
+
+
+ return api;
+ }();
+ })();
+
+ (function () {
+ // a basic progress bar object
+ var ProgressBar = me.Renderable.extend({
+ /**
+ * @ignore
+ */
+ init: function init(x, y, w, h) {
+ this._super(me.Renderable, "init", [x, y, w, h]); // flag to know if we need to refresh the display
+
+
+ this.invalidate = false; // current progress
+
+ this.progress = 0;
+ this.anchorPoint.set(0, 0);
+ },
+
+ /**
+ * make sure the screen is refreshed every frame
+ * @ignore
+ */
+ onProgressUpdate: function onProgressUpdate(progress) {
+ this.progress = ~~(progress * this.width);
+ this.invalidate = true;
+ },
+
+ /**
+ * @ignore
+ */
+ update: function update() {
+ if (this.invalidate === true) {
+ // clear the flag
+ this.invalidate = false; // and return true
+
+ return true;
+ } // else return false
+
+
+ return false;
+ },
+
+ /**
+ * draw function
+ * @ignore
+ */
+ draw: function draw(renderer) {
+ var color = renderer.getColor();
+ var height = renderer.getHeight(); // draw the progress bar
+
+ renderer.setColor("black");
+ renderer.fillRect(this.pos.x, height / 2, this.width, this.height / 2);
+ renderer.setColor("#55aa00");
+ renderer.fillRect(this.pos.x, height / 2, this.progress, this.height / 2);
+ renderer.setColor(color);
+ }
+ }); // the melonJS Logo
+
+ var IconLogo = me.Renderable.extend({
+ /**
+ * @ignore
+ */
+ init: function init(x, y) {
+ this._super(me.Renderable, "init", [x, y, 100, 85]);
+
+ this.iconCanvas = me.video.createCanvas(me.Math.nextPowerOfTwo(this.width), me.Math.nextPowerOfTwo(this.height), false);
+ var context = me.video.renderer.getContext2d(this.iconCanvas);
+ context.beginPath();
+ context.moveTo(0.7, 48.9);
+ context.bezierCurveTo(10.8, 68.9, 38.4, 75.8, 62.2, 64.5);
+ context.bezierCurveTo(86.1, 53.1, 97.2, 27.7, 87.0, 7.7);
+ context.lineTo(87.0, 7.7);
+ context.bezierCurveTo(89.9, 15.4, 73.9, 30.2, 50.5, 41.4);
+ context.bezierCurveTo(27.1, 52.5, 5.2, 55.8, 0.7, 48.9);
+ context.lineTo(0.7, 48.9);
+ context.closePath();
+ context.fillStyle = "rgb(255, 255, 255)";
+ context.fill();
+ context.beginPath();
+ context.moveTo(84.0, 7.0);
+ context.bezierCurveTo(87.6, 14.7, 72.5, 30.2, 50.2, 41.6);
+ context.bezierCurveTo(27.9, 53.0, 6.9, 55.9, 3.2, 48.2);
+ context.bezierCurveTo(-0.5, 40.4, 14.6, 24.9, 36.9, 13.5);
+ context.bezierCurveTo(59.2, 2.2, 80.3, -0.8, 84.0, 7.0);
+ context.lineTo(84.0, 7.0);
+ context.closePath();
+ context.lineWidth = 5.3;
+ context.strokeStyle = "rgb(255, 255, 255)";
+ context.lineJoin = "miter";
+ context.miterLimit = 4.0;
+ context.stroke();
+ this.anchorPoint.set(0.5, 0.5);
+ },
+
+ /**
+ * @ignore
+ */
+ draw: function draw(renderer) {
+ renderer.drawImage(this.iconCanvas, this.pos.x, this.pos.y);
+ }
+ }); // the melonJS Text Logo
+
+ var TextLogo = me.Renderable.extend({
+ /**
+ * @ignore
+ */
+ init: function init(w, h) {
+ this._super(me.Renderable, "init", [0, 0, w, h]); // offscreen cache canvas
+
+
+ this.fontCanvas = me.video.createCanvas(256, 64);
+ this.drawFont(me.video.renderer.getContext2d(this.fontCanvas));
+ this.anchorPoint.set(0.0, 0.0);
+ },
+ drawFont: function drawFont(context) {
+ var logo1 = me.pool.pull("me.Text", 0, 0, {
+ font: "century gothic",
+ size: 32,
+ fillStyle: "white",
+ textAlign: "middle",
+ textBaseline: "top",
+ text: "melon"
+ });
+ var logo2 = me.pool.pull("me.Text", 0, 0, {
+ font: "century gothic",
+ size: 32,
+ fillStyle: "#55aa00",
+ textAlign: "middle",
+ textBaseline: "top",
+ bold: true,
+ text: "JS"
+ }); // compute both logo respective size
+
+ var logo1_width = logo1.measureText(context).width;
+ var logo2_width = logo2.measureText(context).width; // calculate the final rendering position
+
+ this.pos.x = Math.round((this.width - logo1_width - logo2_width) / 2);
+ this.pos.y = Math.round(this.height / 2 + 16); // use the private _drawFont method to directly draw on the canvas context
+
+ logo1._drawFont(context, "melon", 0, 0);
+
+ logo2._drawFont(context, "JS", logo1_width, 0); // put them back into the object pool
+
+
+ me.pool.push(logo1);
+ me.pool.push(logo2);
+ },
+
+ /**
+ * @ignore
+ */
+ draw: function draw(renderer) {
+ renderer.drawImage(this.fontCanvas, this.pos.x, this.pos.y);
+ }
+ });
+ /**
+ * a default loading screen
+ * @memberOf me
+ * @ignore
+ * @constructor
+ */
+
+ me.DefaultLoadingScreen = me.Stage.extend({
+ /**
+ * call when the loader is resetted
+ * @ignore
+ */
+ onResetEvent: function onResetEvent() {
+ // background color
+ me.game.world.addChild(new me.ColorLayer("background", "#202020", 0), 0); // progress bar
+
+ var progressBar = new ProgressBar(0, me.video.renderer.getHeight() / 2, me.video.renderer.getWidth(), 8 // bar height
+ );
+ this.loaderHdlr = me.event.subscribe(me.event.LOADER_PROGRESS, progressBar.onProgressUpdate.bind(progressBar));
+ this.resizeHdlr = me.event.subscribe(me.event.VIEWPORT_ONRESIZE, progressBar.resize.bind(progressBar));
+ me.game.world.addChild(progressBar, 1); // melonJS text & logo
+
+ var icon = new IconLogo(me.video.renderer.getWidth() / 2, me.video.renderer.getHeight() / 2 - progressBar.height - 35);
+ me.game.world.addChild(icon, 1);
+ me.game.world.addChild(new TextLogo(me.video.renderer.getWidth(), me.video.renderer.getHeight()), 1);
+ },
+
+ /**
+ * destroy object at end of loading
+ * @ignore
+ */
+ onDestroyEvent: function onDestroyEvent() {
+ // cancel the callback
+ me.event.unsubscribe(this.loaderHdlr);
+ me.event.unsubscribe(this.resizeHdlr);
+ this.loaderHdlr = this.resizeHdlr = null;
+ }
+ });
+ })();
+
+ (function () {
+ /**
+ * a small class to manage loading of stuff and manage resources
+ * There is no constructor function for me.input.
+ * @namespace me.loader
+ * @memberOf me
+ */
+ me.loader = function () {
+ // hold public stuff in our singleton
+ var api = {}; // contains all the images loaded
+
+ var imgList = {}; // contains all the TMX loaded
+
+ var tmxList = {}; // contains all the binary files loaded
+
+ var binList = {}; // contains all the JSON files
+
+ var jsonList = {}; // baseURL
+
+ var baseURL = {}; // flag to check loading status
+
+ var resourceCount = 0;
+ var loadCount = 0;
+ var timerId = 0;
+ /**
+ * check the loading status
+ * @ignore
+ */
+
+ function checkLoadStatus(onload) {
+ if (loadCount === resourceCount) {
+ // wait 1/2s and execute callback (cheap workaround to ensure everything is loaded)
+ if (onload || api.onload) {
+ // make sure we clear the timer
+ clearTimeout(timerId); // trigger the onload callback
+ // we call either the supplied callback (which takes precedence) or the global one
+
+ var callback = onload || api.onload;
+ setTimeout(function () {
+ callback();
+ me.event.publish(me.event.LOADER_COMPLETE);
+ }, 300);
+ } else {
+ throw new Error("no load callback defined");
+ }
+ } else {
+ timerId = setTimeout(function () {
+ checkLoadStatus(onload);
+ }, 100);
+ }
+ }
+ /**
+ * load Images
+ * @example
+ * preloadImages([
+ * { name : 'image1', src : 'images/image1.png'},
+ * { name : 'image2', src : 'images/image2.png'},
+ * { name : 'image3', src : 'images/image3.png'},
+ * { name : 'image4', src : 'images/image4.png'}
+ * ]);
+ * @ignore
+ */
+
+
+ function preloadImage(img, onload, onerror) {
+ // create new Image object and add to list
+ imgList[img.name] = new Image();
+ imgList[img.name].onload = onload;
+ imgList[img.name].onerror = onerror;
+
+ if (typeof api.crossOrigin === "string") {
+ imgList[img.name].crossOrigin = api.crossOrigin;
+ }
+
+ imgList[img.name].src = img.src + api.nocache;
+ }
+ /**
+ * load a font face
+ * @example
+ * preloadFontFace(
+ * name: "'kenpixel'", type: "fontface", src: "url('data/font/kenvector_future.woff2')"
+ * ]);
+ * @ignore
+ */
+
+
+ function preloadFontFace(data, onload, onerror) {
+ var font = new FontFace(data.name, data.src); // loading promise
+
+ font.load().then(function () {
+ // apply the font after the font has finished downloading
+ document.fonts.add(font);
+ document.body.style.fontFamily = data.name; // onloaded callback
+
+ onload();
+ }, function (e) {
+ // rejected
+ onerror(data.name);
+ });
+ }
+ /**
+ * preload TMX files
+ * @ignore
+ */
+
+
+ function preloadTMX(tmxData, onload, onerror) {
+ function addToTMXList(data) {
+ // set the TMX content
+ tmxList[tmxData.name] = data; // add the tmx to the levelDirector
+
+ if (tmxData.type === "tmx") {
+ me.levelDirector.addTMXLevel(tmxData.name);
+ }
+ } //if the data is in the tmxData object, don't get it via a XMLHTTPRequest
+
+
+ if (tmxData.data) {
+ addToTMXList(tmxData.data);
+ onload();
+ return;
+ }
+
+ var xmlhttp = new XMLHttpRequest(); // check the data format ('tmx', 'json')
+
+ var format = me.utils.file.getExtension(tmxData.src);
+
+ if (xmlhttp.overrideMimeType) {
+ if (format === "json") {
+ xmlhttp.overrideMimeType("application/json");
+ } else {
+ xmlhttp.overrideMimeType("text/xml");
+ }
+ }
+
+ xmlhttp.open("GET", tmxData.src + api.nocache, true);
+ xmlhttp.withCredentials = me.loader.withCredentials; // set the callbacks
+
+ xmlhttp.ontimeout = onerror;
+
+ xmlhttp.onreadystatechange = function () {
+ if (xmlhttp.readyState === 4) {
+ // status = 0 when file protocol is used, or cross-domain origin,
+ // (With Chrome use "--allow-file-access-from-files --disable-web-security")
+ if (xmlhttp.status === 200 || xmlhttp.status === 0 && xmlhttp.responseText) {
+ var result = null; // parse response
+
+ switch (format) {
+ case "xml":
+ case "tmx":
+ case "tsx":
+ // ie9 does not fully implement the responseXML
+ if (me.device.ua.match(/msie/i) || !xmlhttp.responseXML) {
+ if (window.DOMParser) {
+ // manually create the XML DOM
+ result = new DOMParser().parseFromString(xmlhttp.responseText, "text/xml");
+ } else {
+ throw new Error("XML file format loading not supported, use the JSON file format instead");
+ }
+ } else {
+ result = xmlhttp.responseXML;
+ } // converts to a JS object
+
+
+ var data = me.TMXUtils.parse(result);
+
+ switch (format) {
+ case "tmx":
+ result = data.map;
+ break;
+
+ case "tsx":
+ result = data.tilesets[0];
+ break;
+ }
+
+ break;
+
+ case "json":
+ result = JSON.parse(xmlhttp.responseText);
+ break;
+
+ default:
+ throw new Error("TMX file format " + format + "not supported !");
+ } //set the TMX content
+
+
+ addToTMXList(result); // fire the callback
+
+ onload();
+ } else {
+ onerror(tmxData.name);
+ }
+ }
+ }; // send the request
+
+
+ xmlhttp.send();
+ }
+ /**
+ * preload JSON files
+ * @ignore
+ */
+
+
+ function preloadJSON(data, onload, onerror) {
+ var xmlhttp = new XMLHttpRequest();
+
+ if (xmlhttp.overrideMimeType) {
+ xmlhttp.overrideMimeType("application/json");
+ }
+
+ xmlhttp.open("GET", data.src + api.nocache, true);
+ xmlhttp.withCredentials = me.loader.withCredentials; // set the callbacks
+
+ xmlhttp.ontimeout = onerror;
+
+ xmlhttp.onreadystatechange = function () {
+ if (xmlhttp.readyState === 4) {
+ // status = 0 when file protocol is used, or cross-domain origin,
+ // (With Chrome use "--allow-file-access-from-files --disable-web-security")
+ if (xmlhttp.status === 200 || xmlhttp.status === 0 && xmlhttp.responseText) {
+ // get the Texture Packer Atlas content
+ jsonList[data.name] = JSON.parse(xmlhttp.responseText); // fire the callback
+
+ onload();
+ } else {
+ onerror(data.name);
+ }
+ }
+ }; // send the request
+
+
+ xmlhttp.send();
+ }
+ /**
+ * preload Binary files
+ * @ignore
+ */
+
+
+ function preloadBinary(data, onload, onerror) {
+ var httpReq = new XMLHttpRequest(); // load our file
+
+ httpReq.open("GET", data.src + api.nocache, true);
+ httpReq.withCredentials = me.loader.withCredentials;
+ httpReq.responseType = "arraybuffer";
+ httpReq.onerror = onerror;
+
+ httpReq.onload = function () {
+ var arrayBuffer = httpReq.response;
+
+ if (arrayBuffer) {
+ var byteArray = new Uint8Array(arrayBuffer);
+ var buffer = [];
+
+ for (var i = 0; i < byteArray.byteLength; i++) {
+ buffer[i] = String.fromCharCode(byteArray[i]);
+ }
+
+ binList[data.name] = buffer.join(""); // callback
+
+ onload();
+ }
+ };
+
+ httpReq.send();
+ }
+ /**
+ * preload Binary files
+ * @ignore
+ */
+
+
+ function preloadJavascript(data, onload, onerror) {
+ var script = document.createElement("script");
+ script.src = data.src;
+ script.type = "text/javascript";
+
+ if (typeof api.crossOrigin === "string") {
+ script.crossOrigin = api.crossOrigin;
+ }
+
+ script.defer = true;
+
+ script.onload = function () {
+ // callback
+ onload();
+ };
+
+ script.onerror = function () {
+ // callback
+ onerror(data.name);
+ };
+
+ document.getElementsByTagName("body")[0].appendChild(script);
+ }
+ /**
+ * to enable/disable caching
+ * @ignore
+ */
+
+
+ api.nocache = "";
+ /*
+ * PUBLIC STUFF
+ */
+
+ /**
+ * onload callback
+ * @public
+ * @function
+ * @name onload
+ * @memberOf me.loader
+ * @example
+ * // set a callback when everything is loaded
+ * me.loader.onload = this.loaded.bind(this);
+ */
+
+ api.onload = undefined;
+ /**
+ * onProgress callback
+ * each time a resource is loaded, the loader will fire the specified function,
+ * giving the actual progress [0 ... 1], as argument, and an object describing the resource loaded
+ * @public
+ * @function
+ * @name onProgress
+ * @memberOf me.loader
+ * @example
+ * // set a callback for progress notification
+ * me.loader.onProgress = this.updateProgress.bind(this);
+ */
+
+ api.onProgress = undefined;
+ /**
+ * crossOrigin attribute to configure the CORS requests for Image data element.
+ * By default (that is, when the attribute is not specified), CORS is not used at all.
+ * The "anonymous" keyword means that there will be no exchange of user credentials via cookies,
+ * client-side SSL certificates or HTTP authentication as described in the Terminology section of the CORS specification.
+ * @public
+ * @type String
+ * @name crossOrigin
+ * @default undefined
+ * @memberOf me.loader
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes
+ * @example
+ * // allow for cross-origin texture loading in WebGL
+ * me.loader.crossOrigin = "anonymous";
+ *
+ * // set all ressources to be loaded
+ * me.loader.preload(game.resources, this.loaded.bind(this));
+ */
+
+ api.crossOrigin = undefined;
+ /**
+ * indicates whether or not cross-site Access-Control requests should be made using credentials such as cookies,
+ * authorization headers or TLS client certificates. Setting withCredentials has no effect on same-site requests.
+ * @public
+ * @type Boolean
+ * @name withCredentials
+ * @default false
+ * @memberOf me.loader
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials
+ * @example
+ * // enable withCredentials
+ * me.loader.withCredentials = true;
+ *
+ * // set all ressources to be loaded
+ * me.loader.preload(game.resources, this.loaded.bind(this));
+ */
+
+ api.withCredentials = false;
+ /**
+ * just increment the number of already loaded resources
+ * @ignore
+ */
+
+ api.onResourceLoaded = function (res) {
+ // increment the loading counter
+ loadCount++; // currrent progress
+
+ var progress = loadCount / resourceCount; // call callback if defined
+
+ if (api.onProgress) {
+ // pass the load progress in percent, as parameter
+ api.onProgress(progress, res);
+ }
+
+ me.event.publish(me.event.LOADER_PROGRESS, [progress, res]);
+ };
+ /**
+ * on error callback for image loading
+ * @ignore
+ */
+
+
+ api.onLoadingError = function (res) {
+ throw new Error("Failed loading resource " + res.src);
+ };
+ /**
+ * enable the nocache mechanism
+ * @ignore
+ */
+
+
+ api.setNocache = function (enable) {
+ api.nocache = enable ? "?" + ~~(Math.random() * 10000000) : "";
+ };
+ /**
+ * change the default baseURL for the given asset type.
+ * (this will prepend the asset URL and must finish with a '/')
+ * @name setBaseURL
+ * @memberOf me.loader
+ * @public
+ * @function
+ * @param {String} type "*", "audio", binary", "image", "json", "js", "tmx", "tsx"
+ * @param {String} [url="./"] default base URL
+ * @example
+ * // change the base URL relative address for audio assets
+ * me.loader.setBaseURL("audio", "data/audio/");
+ * // change the base URL absolute address for all object types
+ * me.loader.setBaseURL("*", "http://myurl.com/")
+ */
+
+
+ api.setBaseURL = function (type, url) {
+ if (type !== "*") {
+ baseURL[type] = url;
+ } else {
+ // "wildcards"
+ baseURL["audio"] = url;
+ baseURL["binary"] = url;
+ baseURL["image"] = url;
+ baseURL["json"] = url;
+ baseURL["js"] = url;
+ baseURL["tmx"] = url;
+ baseURL["tsx"] = url; // XXX ?
+ //baseURL["fontface"] = url;
+ }
+ };
+ /**
+ * set all the specified game resources to be preloaded.
+ * @name preload
+ * @memberOf me.loader
+ * @public
+ * @function
+ * @param {Object[]} resources
+ * @param {String} resources.name internal name of the resource
+ * @param {String} resources.type "audio", binary", "image", "json","js", "tmx", "tsx", "fontface"
+ * @param {String} resources.src path and/or file name of the resource (for audio assets only the path is required)
+ * @param {Boolean} [resources.stream] Set to true to force HTML5 Audio, which allows not to wait for large file to be downloaded before playing.
+ * @param {function} [onload=me.loader.onload] function to be called when all resources are loaded
+ * @param {boolean} [switchToLoadState=true] automatically switch to the loading screen
+ * @example
+ * game_resources = [
+ * // PNG tileset
+ * {name: "tileset-platformer", type: "image", src: "data/map/tileset.png"},
+ * // PNG packed texture
+ * {name: "texture", type:"image", src: "data/gfx/texture.png"}
+ * // TSX file
+ * {name: "meta_tiles", type: "tsx", src: "data/map/meta_tiles.tsx"},
+ * // TMX level (XML & JSON)
+ * {name: "map1", type: "tmx", src: "data/map/map1.json"},
+ * {name: "map2", type: "tmx", src: "data/map/map2.tmx"},
+ * {name: "map3", type: "tmx", format: "json", data: {"height":15,"layers":[...],"tilewidth":32,"version":1,"width":20}},
+ * {name: "map4", type: "tmx", format: "xml", data: {xml representation of tmx}},
+ * // audio resources
+ * {name: "bgmusic", type: "audio", src: "data/audio/"},
+ * {name: "cling", type: "audio", src: "data/audio/"},
+ * // binary file
+ * {name: "ymTrack", type: "binary", src: "data/audio/main.ym"},
+ * // JSON file (used for texturePacker)
+ * {name: "texture", type: "json", src: "data/gfx/texture.json"},
+ * // JavaScript file
+ * {name: "plugin", type: "js", src: "data/js/plugin.js"},
+ * // Font Face
+ * { name: "'kenpixel'", type: "fontface", src: "url('data/font/kenvector_future.woff2')" }
+ * ];
+ * ...
+ * // set all resources to be loaded
+ * me.loader.preload(game.resources, this.loaded.bind(this));
+ */
+
+
+ api.preload = function (res, onload, switchToLoadState) {
+ // parse the resources
+ for (var i = 0; i < res.length; i++) {
+ resourceCount += api.load(res[i], api.onResourceLoaded.bind(api, res[i]), api.onLoadingError.bind(api, res[i]));
+ } // set the onload callback if defined
+
+
+ if (typeof onload !== "undefined") {
+ api.onload = onload;
+ }
+
+ if (switchToLoadState !== false) {
+ // swith to the loading screen
+ me.state.change(me.state.LOADING);
+ } // check load status
+
+
+ checkLoadStatus(onload);
+ };
+ /**
+ * Load a single resource (to be used if you need to load additional resource during the game)
+ * @name load
+ * @memberOf me.loader
+ * @public
+ * @function
+ * @param {Object} resource
+ * @param {String} resource.name internal name of the resource
+ * @param {String} resource.type "audio", binary", "image", "json", "tmx", "tsx"
+ * @param {String} resource.src path and/or file name of the resource (for audio assets only the path is required)
+ * @param {Boolean} [resource.stream] Set to true to force HTML5 Audio, which allows not to wait for large file to be downloaded before playing.
+ * @param {Function} onload function to be called when the resource is loaded
+ * @param {Function} onerror function to be called in case of error
+ * @example
+ * // load an image asset
+ * me.loader.load({name: "avatar", type:"image", src: "data/avatar.png"}, this.onload.bind(this), this.onerror.bind(this));
+ *
+ * // start loading music
+ * me.loader.load({
+ * name : "bgmusic",
+ * type : "audio",
+ * src : "data/audio/"
+ * }, function () {
+ * me.audio.play("bgmusic");
+ * });
+ */
+
+
+ api.load = function (res, onload, onerror) {
+ // transform the url if necessary
+ if (typeof baseURL[res.type] !== "undefined") {
+ res.src = baseURL[res.type] + res.src;
+ } // check ressource type
+
+
+ switch (res.type) {
+ case "binary":
+ // reuse the preloadImage fn
+ preloadBinary.call(this, res, onload, onerror);
+ return 1;
+
+ case "image":
+ // reuse the preloadImage fn
+ preloadImage.call(this, res, onload, onerror);
+ return 1;
+
+ case "json":
+ preloadJSON.call(this, res, onload, onerror);
+ return 1;
+
+ case "js":
+ preloadJavascript.call(this, res, onload, onerror);
+ return 1;
+
+ case "tmx":
+ case "tsx":
+ preloadTMX.call(this, res, onload, onerror);
+ return 1;
+
+ case "audio":
+ me.audio.load(res, !!res.stream, onload, onerror);
+ return 1;
+
+ case "fontface":
+ preloadFontFace.call(this, res, onload, onerror);
+ return 1;
+
+ default:
+ throw new Error("load : unknown or invalid resource type : " + res.type);
+ }
+ };
+ /**
+ * unload specified resource to free memory
+ * @name unload
+ * @memberOf me.loader
+ * @public
+ * @function
+ * @param {Object} resource
+ * @return {Boolean} true if unloaded
+ * @example me.loader.unload({name: "avatar", type:"image", src: "data/avatar.png"});
+ */
+
+
+ api.unload = function (res) {
+ switch (res.type) {
+ case "binary":
+ if (!(res.name in binList)) {
+ return false;
+ }
+
+ delete binList[res.name];
+ return true;
+
+ case "image":
+ if (!(res.name in imgList)) {
+ return false;
+ }
+
+ delete imgList[res.name];
+ return true;
+
+ case "json":
+ if (!(res.name in jsonList)) {
+ return false;
+ }
+
+ delete jsonList[res.name];
+ return true;
+
+ case "js":
+ // ??
+ return true;
+
+ case "fontface":
+ // ??
+ return true;
+
+ case "tmx":
+ case "tsx":
+ if (!(res.name in tmxList)) {
+ return false;
+ }
+
+ delete tmxList[res.name];
+ return true;
+
+ case "audio":
+ return me.audio.unload(res.name);
+
+ default:
+ throw new Error("unload : unknown or invalid resource type : " + res.type);
+ }
+ };
+ /**
+ * unload all resources to free memory
+ * @name unloadAll
+ * @memberOf me.loader
+ * @public
+ * @function
+ * @example me.loader.unloadAll();
+ */
+
+
+ api.unloadAll = function () {
+ var name; // unload all binary resources
+
+ for (name in binList) {
+ if (binList.hasOwnProperty(name)) {
+ api.unload({
+ "name": name,
+ "type": "binary"
+ });
+ }
+ } // unload all image resources
+
+
+ for (name in imgList) {
+ if (imgList.hasOwnProperty(name)) {
+ api.unload({
+ "name": name,
+ "type": "image"
+ });
+ }
+ } // unload all tmx resources
+
+
+ for (name in tmxList) {
+ if (tmxList.hasOwnProperty(name)) {
+ api.unload({
+ "name": name,
+ "type": "tmx"
+ });
+ }
+ } // unload all in json resources
+
+
+ for (name in jsonList) {
+ if (jsonList.hasOwnProperty(name)) {
+ api.unload({
+ "name": name,
+ "type": "json"
+ });
+ }
+ } // unload all audio resources
+
+
+ me.audio.unloadAll();
+ };
+ /**
+ * return the specified TMX/TSX object
+ * @name getTMX
+ * @memberOf me.loader
+ * @public
+ * @function
+ * @param {String} tmx name of the tmx/tsx element ("map1");
+ * @return {XML|Object} requested element or null if not found
+ */
+
+
+ api.getTMX = function (elt) {
+ // force as string
+ elt = "" + elt;
+
+ if (elt in tmxList) {
+ return tmxList[elt];
+ }
+
+ return null;
+ };
+ /**
+ * return the specified Binary object
+ * @name getBinary
+ * @memberOf me.loader
+ * @public
+ * @function
+ * @param {String} name of the binary object ("ymTrack");
+ * @return {Object} requested element or null if not found
+ */
+
+
+ api.getBinary = function (elt) {
+ // force as string
+ elt = "" + elt;
+
+ if (elt in binList) {
+ return binList[elt];
+ }
+
+ return null;
+ };
+ /**
+ * return the specified Image Object
+ * @name getImage
+ * @memberOf me.loader
+ * @public
+ * @function
+ * @param {String} image name of the Image element ("tileset-platformer");
+ * @return {HTMLImageElement} requested element or null if not found
+ */
+
+
+ api.getImage = function (image) {
+ // force as string and extract the base name
+ image = me.utils.file.getBasename("" + image);
+
+ if (image in imgList) {
+ // return the corresponding Image object
+ return imgList[image];
+ }
+
+ return null;
+ };
+ /**
+ * return the specified JSON Object
+ * @name getJSON
+ * @memberOf me.loader
+ * @public
+ * @function
+ * @param {String} Name for the json file to load
+ * @return {Object}
+ */
+
+
+ api.getJSON = function (elt) {
+ // force as string
+ elt = "" + elt;
+
+ if (elt in jsonList) {
+ return jsonList[elt];
+ }
+
+ return null;
+ };
+ /**
+ * Return the loading progress in percent
+ * @name getLoadProgress
+ * @memberOf me.loader
+ * @public
+ * @function
+ * @deprecated use callback instead
+ * @see me.loader.onProgress
+ * @see me.event.LOADER_PROGRESS
+ * @return {Number}
+ */
+
+
+ api.getLoadProgress = function () {
+ return loadCount / resourceCount;
+ }; // return our object
+
+
+ return api;
+ }();
+ })();
+
+ /*
+ * ASCII Table
+ * http://www.asciitable.com/
+ * [ !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz]
+ *
+ * -> first char " " 32d (0x20);
+ */
+ (function () {
+ var runits = ["ex", "em", "pt", "px"];
+ var toPX = [12, 24, 0.75, 1];
+ /**
+ * apply the current font style to the given context
+ * @ignore
+ */
+
+ var setContextStyle = function setContextStyle(context, font, stroke) {
+ context.font = font.font;
+ context.fillStyle = font.fillStyle.toRGBA();
+
+ if (stroke === true) {
+ context.strokeStyle = font.strokeStyle.toRGBA();
+ context.lineWidth = font.lineWidth;
+ }
+
+ context.textAlign = font.textAlign;
+ context.textBaseline = font.textBaseline;
+ };
+ /**
+ * a generic system font object.
+ * @class
+ * @extends me.Renderable
+ * @memberOf me
+ * @constructor
+ * @param {Number} x position of the text object
+ * @param {Number} y position of the text object
+ * @param {Object} settings the text configuration
+ * @param {String} settings.font a CSS family font name
+ * @param {Number|String} settings.size size, or size + suffix (px, em, pt)
+ * @param {me.Color|String} [settings.fillStyle="#000000"] a CSS color value
+ * @param {me.Color|String} [settings.strokeStyle="#000000"] a CSS color value
+ * @param {Number} [settings.lineWidth=1] line width, in pixels, when drawing stroke
+ * @param {String} [settings.textAlign="left"] horizontal text alignment
+ * @param {String} [settings.textBaseline="top"] the text baseline
+ * @param {Number} [settings.lineHeight=1.0] line spacing height
+ * @param {me.Vector2d} [settings.anchorPoint={x:0.0, y:0.0}] anchor point to draw the text at
+ * @param {(String|String[])} [settings.text] a string, or an array of strings
+ */
+
+
+ me.Text = me.Renderable.extend({
+ /** @ignore */
+ init: function init(x, y, settings) {
+ // call the parent constructor
+ this._super(me.Renderable, "init", [x, y, settings.width || 0, settings.height || 0]);
+ /**
+ * defines the color used to draw the font.
+ * @public
+ * @type me.Color
+ * @default black
+ * @name me.Text#fillStyle
+ */
+
+
+ if (typeof settings.fillStyle !== "undefined") {
+ if (settings.fillStyle instanceof me.Color) {
+ this.fillStyle = settings.fillStyle;
+ } else {
+ // string (#RGB, #ARGB, #RRGGBB, #AARRGGBB)
+ this.fillStyle = me.pool.pull("me.Color").parseCSS(settings.fillStyle);
+ }
+ } else {
+ this.fillStyle = me.pool.pull("me.Color", 0, 0, 0);
+ }
+ /**
+ * defines the color used to draw the font stroke.
+ * @public
+ * @type me.Color
+ * @default black
+ * @name me.Text#strokeStyle
+ */
+
+
+ if (typeof settings.strokeStyle !== "undefined") {
+ if (settings.strokeStyle instanceof me.Color) {
+ this.strokeStyle = settings.strokeStyle;
+ } else {
+ // string (#RGB, #ARGB, #RRGGBB, #AARRGGBB)
+ this.strokeStyle = me.pool.pull("me.Color").parseCSS(settings.strokeStyle);
+ }
+ } else {
+ this.strokeStyle = me.pool.pull("me.Color", 0, 0, 0);
+ }
+ /**
+ * sets the current line width, in pixels, when drawing stroke
+ * @public
+ * @type Number
+ * @default 1
+ * @name me.Text#lineWidth
+ */
+
+
+ this.lineWidth = settings.lineWidth || 1;
+ /**
+ * Set the default text alignment (or justification),
+ * possible values are "left", "right", and "center".
+ * @public
+ * @type String
+ * @default "left"
+ * @name me.Text#textAlign
+ */
+
+ this.textAlign = settings.textAlign || "left";
+ /**
+ * Set the text baseline (e.g. the Y-coordinate for the draw operation),
+ * possible values are "top", "hanging, "middle, "alphabetic, "ideographic, "bottom"
+ * @public
+ * @type String
+ * @default "top"
+ * @name me.Text#textBaseline
+ */
+
+ this.textBaseline = settings.textBaseline || "top";
+ /**
+ * Set the line spacing height (when displaying multi-line strings).
+ * Current font height will be multiplied with this value to set the line height.
+ * @public
+ * @type Number
+ * @default 1.0
+ * @name me.Text#lineHeight
+ */
+
+ this.lineHeight = settings.lineHeight || 1.0; // private font properties
+
+ this._fontSize = 0; // the text displayed by this bitmapFont object
+
+ this._text = ""; // anchor point
+
+ if (typeof settings.anchorPoint !== "undefined") {
+ this.anchorPoint.setV(settings.anchorPoint);
+ } else {
+ this.anchorPoint.set(0, 0);
+ } // if floating was specified through settings
+
+
+ if (typeof settings.floating !== "undefined") {
+ this.floating = !!settings.floating;
+ } // font name and type
+
+
+ this.setFont(settings.font, settings.size); // aditional
+
+ if (settings.bold === true) {
+ this.bold();
+ }
+
+ if (settings.italic === true) {
+ this.italic();
+ } // set the text
+
+
+ this.setText(settings.text);
+ },
+
+ /**
+ * make the font bold
+ * @name bold
+ * @memberOf me.Text.prototype
+ * @function
+ * @return this object for chaining
+ */
+ bold: function bold() {
+ this.font = "bold " + this.font;
+ this.isDirty = true;
+ return this;
+ },
+
+ /**
+ * make the font italic
+ * @name italic
+ * @memberOf me.Text.prototype
+ * @function
+ * @return this object for chaining
+ */
+ italic: function italic() {
+ this.font = "italic " + this.font;
+ this.isDirty = true;
+ return this;
+ },
+
+ /**
+ * set the font family and size
+ * @name setFont
+ * @memberOf me.Text.prototype
+ * @function
+ * @param {String} font a CSS font name
+ * @param {Number|String} size size, or size + suffix (px, em, pt)
+ * @return this object for chaining
+ * @example
+ * font.setFont("Arial", 20);
+ * font.setFont("Arial", "1.5em");
+ */
+ setFont: function setFont(font, size) {
+ // font name and type
+ var font_names = font.split(",").map(function (value) {
+ value = value.trim();
+ return !/(^".*"$)|(^'.*'$)/.test(value) ? "\"" + value + "\"" : value;
+ }); // font size
+
+ if (typeof size === "number") {
+ this._fontSize = size;
+ size += "px";
+ } else
+ /* string */
+ {
+ // extract the units and convert if necessary
+ var CSSval = size.match(/([-+]?[\d.]*)(.*)/);
+ this._fontSize = parseFloat(CSSval[1]);
+
+ if (CSSval[2]) {
+ this._fontSize *= toPX[runits.indexOf(CSSval[2])];
+ } else {
+ // no unit define, assume px
+ size += "px";
+ }
+ }
+
+ this.height = this._fontSize;
+ this.font = size + " " + font_names.join(",");
+ this.isDirty = true;
+ return this;
+ },
+
+ /**
+ * change the text to be displayed
+ * @name setText
+ * @memberOf me.Text.prototype
+ * @function
+ * @param {Number|String|String[]} value a string, or an array of strings
+ * @return this object for chaining
+ */
+ setText: function setText(value) {
+ value = "" + value;
+
+ if (this._text !== value) {
+ if (Array.isArray(value)) {
+ this._text = value.join("\n");
+ } else {
+ this._text = value;
+ }
+
+ this.isDirty = true;
+ }
+
+ return this;
+ },
+
+ /**
+ * measure the given text size in pixels
+ * @name measureText
+ * @memberOf me.Text.prototype
+ * @function
+ * @param {me.CanvasRenderer|me.WebGLRenderer} [renderer] reference a renderer instance
+ * @param {String} [text] the text to be measured
+ * @param {me.Rect} [ret] a object in which to store the text metrics
+ * @returns {TextMetrics} a TextMetrics object with two properties: `width` and `height`, defining the output dimensions
+ */
+ measureText: function measureText(renderer, text, ret) {
+ text = text || this._text;
+ var context;
+
+ if (typeof renderer === "undefined") {
+ context = me.video.renderer.getFontContext();
+ } else if (renderer instanceof me.Renderer) {
+ context = renderer.getFontContext();
+ } else {
+ // else it's a 2d rendering context object
+ context = renderer;
+ }
+
+ var textMetrics = ret || this.getBounds();
+ var lineHeight = this._fontSize * this.lineHeight;
+ var strings = ("" + text).split("\n"); // save the previous context
+
+ context.save(); // apply the style font
+
+ setContextStyle(context, this); // compute the bounding box size
+
+ this.height = this.width = 0;
+
+ for (var i = 0; i < strings.length; i++) {
+ this.width = Math.max(context.measureText(me.utils.string.trimRight(strings[i])).width, this.width);
+ this.height += lineHeight;
+ }
+
+ textMetrics.width = Math.ceil(this.width);
+ textMetrics.height = Math.ceil(this.height); // compute the bounding box position
+
+ textMetrics.pos.x = Math.floor(this.textAlign === "right" ? this.pos.x - this.width : this.textAlign === "center" ? this.pos.x - this.width / 2 : this.pos.x);
+ textMetrics.pos.y = Math.floor(this.textBaseline.search(/^(top|hanging)$/) === 0 ? this.pos.y : this.textBaseline === "middle" ? this.pos.y - textMetrics.height / 2 : this.pos.y - textMetrics.height); // restore the context
+
+ context.restore(); // returns the Font bounds me.Rect by default
+
+ return textMetrics;
+ },
+
+ /**
+ * @ignore
+ */
+ update: function update()
+ /* dt */
+ {
+ if (this.isDirty === true) {
+ this.measureText();
+ }
+
+ return this.isDirty;
+ },
+
+ /**
+ * draw a text at the specified coord
+ * @name draw
+ * @memberOf me.Text.prototype
+ * @function
+ * @param {me.CanvasRenderer|me.WebGLRenderer} renderer Reference to the destination renderer instance
+ * @param {String} [text]
+ * @param {Number} [x]
+ * @param {Number} [y]
+ */
+ draw: function draw(renderer, text, x, y, stroke) {
+ // "hacky patch" for backward compatibilty
+ if (typeof this.ancestor === "undefined") {
+ // update text cache
+ this.setText(text); // update position if changed
+
+ if (this.pos.x !== x || this.pos.y !== y) {
+ this.pos.x = x;
+ this.pos.y = y;
+ this.isDirty = true;
+ } // force update bounds
+
+
+ this.update(0); // save the previous context
+
+ renderer.save(); // apply the defined alpha value
+
+ renderer.setGlobalAlpha(renderer.globalAlpha() * this.getOpacity());
+ } else {
+ // added directly to an object container
+ x = this.pos.x;
+ y = this.pos.y;
+ }
+
+ if (renderer.settings.subPixel === false) {
+ // clamp to pixel grid if required
+ x = ~~x;
+ y = ~~y;
+ } // draw the text
+
+
+ renderer.drawFont(this._drawFont(renderer.getFontContext(), this._text, x, y, stroke || false)); // for backward compatibilty
+
+ if (typeof this.ancestor === "undefined") {
+ // restore previous context
+ renderer.restore();
+ } // clear the dirty flag
+
+
+ this.isDirty = false;
+ },
+
+ /**
+ * draw a stroke text at the specified coord, as defined
+ * by the `lineWidth` and `fillStroke` properties.
+ * Note : using drawStroke is not recommended for performance reasons
+ * @name drawStroke
+ * @memberOf me.Text.prototype
+ * @function
+ * @param {me.CanvasRenderer|me.WebGLRenderer} renderer Reference to the destination renderer instance
+ * @param {String} text
+ * @param {Number} x
+ * @param {Number} y
+ */
+ drawStroke: function drawStroke(renderer, text, x, y) {
+ this.draw.call(this, renderer, text, x, y, true);
+ },
+
+ /**
+ * @ignore
+ */
+ _drawFont: function _drawFont(context, text, x, y, stroke) {
+ setContextStyle(context, this, stroke);
+ var strings = ("" + text).split("\n");
+ var lineHeight = this._fontSize * this.lineHeight;
+
+ for (var i = 0; i < strings.length; i++) {
+ var string = me.utils.string.trimRight(strings[i]); // draw the string
+
+ context[stroke ? "strokeText" : "fillText"](string, x, y); // add leading space
+
+ y += lineHeight;
+ }
+
+ return this.getBounds();
+ },
+
+ /**
+ * Destroy function
+ * @ignore
+ */
+ destroy: function destroy() {
+ me.pool.push(this.fillStyle);
+ me.pool.push(this.strokeStyle);
+ this.fillStyle = this.strokeStyle = undefined;
+
+ this._super(me.Renderable, "destroy");
+ }
+ });
+ })();
+
+ (function () {
+ /**
+ * Measures the width of a single line of text, does not account for \n
+ * @ignore
+ */
+ var measureTextWidth = function measureTextWidth(font, text) {
+ var characters = text.split("");
+ var width = 0;
+ var lastGlyph = null;
+
+ for (var i = 0; i < characters.length; i++) {
+ var ch = characters[i].charCodeAt(0);
+ var glyph = font.fontData.glyphs[ch];
+ var kerning = lastGlyph && lastGlyph.kerning ? lastGlyph.getKerning(ch) : 0;
+ width += (glyph.xadvance + kerning) * font.fontScale.x;
+ lastGlyph = glyph;
+ }
+
+ return width;
+ };
+ /**
+ * Measures the height of a single line of text, does not account for \n
+ * @ignore
+ */
+
+
+ var measureTextHeight = function measureTextHeight(font) {
+ return font.fontData.capHeight * font.lineHeight * font.fontScale.y;
+ };
+ /**
+ * a bitmap font object
+ * @class
+ * @extends me.Renderable
+ * @memberOf me
+ * @constructor
+ * @param {Number} [scale=1.0]
+ * @param {Object} settings the text configuration
+ * @param {String|Image} settings.font a font name to identify the corresponing source image
+ * @param {String} [settings.fontData=settings.font] the bitmap font data corresponding name, or the bitmap font data itself
+ * @param {Number} [settings.size] size a scaling ratio
+ * @param {Number} [settings.lineWidth=1] line width, in pixels, when drawing stroke
+ * @param {String} [settings.textAlign="left"] horizontal text alignment
+ * @param {String} [settings.textBaseline="top"] the text baseline
+ * @param {Number} [settings.lineHeight=1.0] line spacing height
+ * @param {me.Vector2d} [settings.anchorPoint={x:0.0, y:0.0}] anchor point to draw the text at
+ * @param {(String|String[])} [settings.text] a string, or an array of strings
+ * @example
+ * // Use me.loader.preload or me.loader.load to load assets
+ * me.loader.preload([
+ * { name: "arial", type: "binary" src: "data/font/arial.fnt" },
+ * { name: "arial", type: "image" src: "data/font/arial.png" },
+ * ])
+ * // Then create an instance of your bitmap font:
+ * var myFont = new me.BitmapText(x, y, {font:"arial", text:"Hello"});
+ * // two possibilities for using "myFont"
+ * // either call the draw function from your Renderable draw function
+ * myFont.draw(renderer, "Hello!", 0, 0);
+ * // or just add it to the word container
+ * me.game.world.addChild(myFont);
+ */
+
+
+ me.BitmapText = me.Renderable.extend({
+ /** @ignore */
+ init: function init(x, y, settings) {
+ // call the parent constructor
+ this._super(me.Renderable, "init", [x, y, settings.width || 0, settings.height || 0]);
+ /**
+ * Set the default text alignment (or justification),
+ * possible values are "left", "right", and "center".
+ * @public
+ * @type String
+ * @default "left"
+ * @name textAlign
+ * @memberOf me.BitmapText
+ */
+
+
+ this.textAlign = settings.textAlign || "left";
+ /**
+ * Set the text baseline (e.g. the Y-coordinate for the draw operation),
+ * possible values are "top", "hanging, "middle, "alphabetic, "ideographic, "bottom"
+ * @public
+ * @type String
+ * @default "top"
+ * @name textBaseline
+ * @memberOf me.BitmapText
+ */
+
+ this.textBaseline = settings.textBaseline || "top";
+ /**
+ * Set the line spacing height (when displaying multi-line strings).
+ * Current font height will be multiplied with this value to set the line height.
+ * @public
+ * @type Number
+ * @default 1.0
+ * @name lineHeight
+ * @memberOf me.BitmapText
+ */
+
+ this.lineHeight = settings.lineHeight || 1;
+ /** @ignore */
+ // scaled font size;
+
+ this.fontScale = me.pool.pull("me.Vector2d", 1, 1); // get the corresponding image
+
+ this.fontImage = _typeof(settings.font) === "object" ? settings.font : me.loader.getImage(settings.font);
+
+ if (typeof settings.fontData !== "string") {
+ // use settings.font to retreive the data from the loader
+ this.fontData = me.pool.pull("me.BitmapTextData", me.loader.getBinary(settings.font));
+ } else {
+ this.fontData = me.pool.pull("me.BitmapTextData", // if starting/includes "info face" the whole data string was passed as parameter
+ settings.fontData.includes("info face") ? settings.fontData : me.loader.getBinary(settings.fontData));
+ }
+
+ if (typeof settings.floating !== "undefined") {
+ this.floating = !!settings.floating;
+ } // resize if necessary
+
+
+ if (typeof settings.size === "number" && settings.size !== 1.0) {
+ this.resize(settings.size);
+ } // update anchorPoint if provided
+
+
+ if (typeof settings.anchorPoint !== "undefined") {
+ this.anchorPoint.set(settings.anchorPoint.x, settings.anchorPoint.y);
+ } else {
+ this.anchorPoint.set(0, 0);
+ } // set the text
+
+
+ this.setText(settings.text);
+ },
+
+ /**
+ * change the font settings
+ * @name set
+ * @memberOf me.BitmapText.prototype
+ * @function
+ * @param {String} textAlign ("left", "center", "right")
+ * @param {Number} [scale]
+ * @return this object for chaining
+ */
+ set: function set(textAlign, scale) {
+ this.textAlign = textAlign; // updated scaled Size
+
+ if (scale) {
+ this.resize(scale);
+ }
+
+ this.isDirty = true;
+ return this;
+ },
+
+ /**
+ * change the text to be displayed
+ * @name setText
+ * @memberOf me.BitmapText.prototype
+ * @function
+ * @param {Number|String|String[]} value a string, or an array of strings
+ * @return this object for chaining
+ */
+ setText: function setText(value) {
+ value = "" + value;
+
+ if (this._text !== value) {
+ if (Array.isArray(value)) {
+ this._text = value.join("\n");
+ } else {
+ this._text = value;
+ }
+
+ this.isDirty = true;
+ }
+
+ return this;
+ },
+
+ /**
+ * change the font display size
+ * @name resize
+ * @memberOf me.BitmapText.prototype
+ * @function
+ * @param {Number} scale ratio
+ * @return this object for chaining
+ */
+ resize: function resize(scale) {
+ this.fontScale.set(scale, scale); // clear the cache text to recalculate bounds
+
+ this.isDirty = true;
+ return this;
+ },
+
+ /**
+ * measure the given text size in pixels
+ * @name measureText
+ * @memberOf me.BitmapText.prototype
+ * @function
+ * @param {String} [text]
+ * @param {me.Rect} [ret] a object in which to store the text metrics
+ * @returns {TextMetrics} a TextMetrics object with two properties: `width` and `height`, defining the output dimensions
+ */
+ measureText: function measureText(text, ret) {
+ text = text || this._text;
+ var strings = ("" + text).split("\n");
+ var stringHeight = measureTextHeight(this);
+ var textMetrics = ret || this.getBounds();
+ textMetrics.height = textMetrics.width = 0;
+
+ for (var i = 0; i < strings.length; i++) {
+ textMetrics.width = Math.max(measureTextWidth(this, strings[i]), textMetrics.width);
+ textMetrics.height += stringHeight;
+ }
+
+ return textMetrics;
+ },
+
+ /**
+ * @ignore
+ */
+ update: function update()
+ /* dt */
+ {
+ if (this.isDirty === true) {
+ this.measureText();
+ }
+
+ return this.isDirty;
+ },
+
+ /**
+ * draw the bitmap font
+ * @name draw
+ * @memberOf me.BitmapText.prototype
+ * @function
+ * @param {me.CanvasRenderer|me.WebGLRenderer} renderer Reference to the destination renderer instance
+ * @param {String} [text]
+ * @param {Number} [x]
+ * @param {Number} [y]
+ */
+ draw: function draw(renderer, text, x, y) {
+ // allows to provide backward compatibility when
+ // adding Bitmap Font to an object container
+ if (typeof this.ancestor === "undefined") {
+ // update cache
+ this.setText(text); // force update bounds
+
+ this.update(0); // save the previous global alpha value
+
+ var _alpha = renderer.globalAlpha();
+
+ renderer.setGlobalAlpha(_alpha * this.getOpacity());
+ } else {
+ // added directly to an object container
+ x = this.pos.x;
+ y = this.pos.y;
+ }
+
+ var strings = ("" + this._text).split("\n");
+
+ var lX = x;
+ var stringHeight = measureTextHeight(this);
+ var maxWidth = 0;
+
+ for (var i = 0; i < strings.length; i++) {
+ x = lX;
+ var string = me.utils.string.trimRight(strings[i]); // adjust x pos based on alignment value
+
+ var stringWidth = measureTextWidth(this, string);
+
+ switch (this.textAlign) {
+ case "right":
+ x -= stringWidth;
+ break;
+
+ case "center":
+ x -= stringWidth * 0.5;
+ break;
+
+ default:
+ break;
+ } // adjust y pos based on alignment value
+
+
+ switch (this.textBaseline) {
+ case "middle":
+ y -= stringHeight * 0.5;
+ break;
+
+ case "ideographic":
+ case "alphabetic":
+ case "bottom":
+ y -= stringHeight;
+ break;
+
+ default:
+ break;
+ } // update initial position if required
+
+
+ if (this.isDirty === true && typeof this.ancestor === "undefined") {
+ if (i === 0) {
+ this.pos.y = y;
+ }
+
+ if (maxWidth < stringWidth) {
+ maxWidth = stringWidth;
+ this.pos.x = x;
+ }
+ } // draw the string
+
+
+ var lastGlyph = null;
+
+ for (var c = 0, len = string.length; c < len; c++) {
+ // calculate the char index
+ var ch = string.charCodeAt(c);
+ var glyph = this.fontData.glyphs[ch];
+ var glyphWidth = glyph.width;
+ var glyphHeight = glyph.height;
+ var kerning = lastGlyph && lastGlyph.kerning ? lastGlyph.getKerning(ch) : 0; // draw it
+
+ if (glyphWidth !== 0 && glyphHeight !== 0) {
+ // some browser throw an exception when drawing a 0 width or height image
+ renderer.drawImage(this.fontImage, glyph.x, glyph.y, glyphWidth, glyphHeight, x + glyph.xoffset, y + glyph.yoffset * this.fontScale.y, glyphWidth * this.fontScale.x, glyphHeight * this.fontScale.y);
+ } // increment position
+
+
+ x += (glyph.xadvance + kerning) * this.fontScale.x;
+ lastGlyph = glyph;
+ } // increment line
+
+
+ y += stringHeight;
+ }
+
+ if (typeof this.ancestor === "undefined") {
+ // restore the previous global alpha value
+ renderer.setGlobalAlpha(_alpha);
+ } // clear the dirty flag
+
+
+ this.isDirty = false;
+ },
+
+ /**
+ * Destroy function
+ * @ignore
+ */
+ destroy: function destroy() {
+ me.pool.push(this.fontScale);
+ this.fontScale = undefined;
+ me.pool.push(this.fontData);
+ this.fontData = undefined;
+
+ this._super(me.Renderable, "destroy");
+ }
+ });
+ })();
+
+ (function () {
+ // bitmap constants
+ var LOG2_PAGE_SIZE = 9;
+ var PAGE_SIZE = 1 << LOG2_PAGE_SIZE;
+ var xChars = ["x", "e", "a", "o", "n", "s", "r", "c", "u", "m", "v", "w", "z"];
+ var capChars = ["M", "N", "B", "D", "C", "E", "F", "K", "A", "G", "H", "I", "J", "L", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"];
+ /**
+ * a glyph representing a single character in a font
+ */
+
+ var Glyph = me.Object.extend({
+ /**
+ * @ignore
+ */
+ init: function init() {
+ this.id = 0;
+ this.x = 0;
+ this.y = 0;
+ this.width = 0;
+ this.height = 0;
+ this.u = 0;
+ this.v = 0;
+ this.u2 = 0;
+ this.v2 = 0;
+ this.xoffset = 0;
+ this.yoffset = 0;
+ this.xadvance = 0;
+ this.fixedWidth = false;
+ },
+
+ /**
+ * @ignore
+ */
+ getKerning: function getKerning(ch) {
+ if (this.kerning) {
+ var page = this.kerning[ch >>> LOG2_PAGE_SIZE];
+
+ if (page) {
+ return page[ch & PAGE_SIZE - 1] || 0;
+ }
+ }
+
+ return 0;
+ },
+
+ /**
+ * @ignore
+ */
+ setKerning: function setKerning(ch, value) {
+ if (!this.kerning) {
+ this.kerning = {};
+ }
+
+ var page = this.kerning[ch >>> LOG2_PAGE_SIZE];
+
+ if (typeof page === "undefined") {
+ this.kerning[ch >>> LOG2_PAGE_SIZE] = {};
+ page = this.kerning[ch >>> LOG2_PAGE_SIZE];
+ }
+
+ page[ch & PAGE_SIZE - 1] = value;
+ }
+ });
+ /**
+ * Class for storing relevant data from the font file.
+ * @class me.BitmapTextData
+ * @memberOf me
+ * @param data {String} - The bitmap font data pulled from the resource loader using me.loader.getBinary()
+ * @constructor
+ */
+
+ me.BitmapTextData = me.Object.extend({
+ /**
+ * @ignore
+ */
+ init: function init(data) {
+ this.padTop = 0;
+ this.padRight = 0;
+ this.padBottom = 0;
+ this.padLeft = 0;
+ this.lineHeight = 0; // The distance from the top of most uppercase characters to the baseline. Since the drawing position is the cap height of
+ // the first line, the cap height can be used to get the location of the baseline.
+
+ this.capHeight = 1; // The distance from the bottom of the glyph that extends the lowest to the baseline. This number is negative.
+
+ this.descent = 0;
+ /**
+ * The map of glyphs, each key is a char code.
+ * @name glyphs
+ * @type {Object}
+ * @memberOf me.BitmapTextData
+ */
+
+ this.glyphs = {}; // parse the data
+
+ this.parse(data);
+ },
+
+ /**
+ * Creates a glyph to use for the space character
+ * @private
+ * @name _createSpaceGlyph
+ * @memberOf me.BitmapTextData
+ * @function
+ */
+ _createSpaceGlyph: function _createSpaceGlyph() {
+ var spaceCharCode = " ".charCodeAt(0);
+ var glyph = this.glyphs[spaceCharCode];
+
+ if (!glyph) {
+ glyph = new Glyph();
+ glyph.id = spaceCharCode;
+ glyph.xadvance = this._getFirstGlyph().xadvance;
+ this.glyphs[spaceCharCode] = glyph;
+ }
+ },
+
+ /**
+ * Gets the first glyph in the map that is not a space character
+ * @private
+ * @name _getFirstGlyph
+ * @memberOf me.BitmapTextData
+ * @function
+ * @returns {me.Glyph}
+ */
+ _getFirstGlyph: function _getFirstGlyph() {
+ var keys = Object.keys(this.glyphs);
+
+ for (var i = 0; i < keys.length; i++) {
+ if (keys[i] > 32) {
+ return this.glyphs[keys[i]];
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Gets the value from a string of pairs. For example: one=1 two=2 something=hi. Can accept the regex of /one={d}/
+ * and returns the value of d
+ * @private
+ * @name _getValueFromPair
+ * @memberOf me.BitmapTextData
+ * @function
+ * @returns {String}
+ */
+ _getValueFromPair: function _getValueFromPair(string, pattern) {
+ var value = string.match(pattern);
+
+ if (!value) {
+ throw new Error("Could not find pattern " + pattern + " in string: " + string);
+ }
+
+ return value[0].split("=")[1];
+ },
+
+ /**
+ * This parses the font data text and builds a map of glyphs containing the data for each character
+ * @name parse
+ * @memberOf me.BitmapTextData
+ * @function
+ * @param {String} fontData
+ */
+ parse: function parse(fontData) {
+ if (!fontData) {
+ throw new Error("File containing font data was empty, cannot load the bitmap font.");
+ }
+
+ var lines = fontData.split(/\r\n|\n/);
+ var padding = fontData.match(/padding\=\d+,\d+,\d+,\d+/g);
+
+ if (!padding) {
+ throw new Error("Padding not found in first line");
+ }
+
+ var paddingValues = padding[0].split("=")[1].split(",");
+ this.padTop = parseFloat(paddingValues[0]);
+ this.padLeft = parseFloat(paddingValues[1]);
+ this.padBottom = parseFloat(paddingValues[2]);
+ this.padRight = parseFloat(paddingValues[3]);
+ this.lineHeight = parseFloat(this._getValueFromPair(lines[1], /lineHeight\=\d+/g));
+ var baseLine = parseFloat(this._getValueFromPair(lines[1], /base\=\d+/g));
+ var padY = this.padTop + this.padBottom;
+ var glyph = null;
+
+ for (var i = 4; i < lines.length; i++) {
+ var line = lines[i];
+ var characterValues = line.split(/=|\s+/);
+
+ if (!line || /^kernings/.test(line)) {
+ continue;
+ }
+
+ if (/^kerning\s/.test(line)) {
+ var first = parseFloat(characterValues[2]);
+ var second = parseFloat(characterValues[4]);
+ var amount = parseFloat(characterValues[6]);
+ glyph = this.glyphs[first];
+
+ if (glyph !== null && typeof glyph !== "undefined") {
+ glyph.setKerning(second, amount);
+ }
+ } else {
+ glyph = new Glyph();
+ var ch = parseFloat(characterValues[2]);
+ glyph.id = ch;
+ glyph.x = parseFloat(characterValues[4]);
+ glyph.y = parseFloat(characterValues[6]);
+ glyph.width = parseFloat(characterValues[8]);
+ glyph.height = parseFloat(characterValues[10]);
+ glyph.xoffset = parseFloat(characterValues[12]);
+ glyph.yoffset = parseFloat(characterValues[14]);
+ glyph.xadvance = parseFloat(characterValues[16]);
+
+ if (glyph.width > 0 && glyph.height > 0) {
+ this.descent = Math.min(baseLine + glyph.yoffset, this.descent);
+ }
+
+ this.glyphs[ch] = glyph;
+ }
+ }
+
+ this.descent += this.padBottom;
+
+ this._createSpaceGlyph();
+
+ var xGlyph = null;
+
+ for (i = 0; i < xChars.length; i++) {
+ var xChar = xChars[i];
+ xGlyph = this.glyphs[xChar.charCodeAt(0)];
+
+ if (xGlyph) {
+ break;
+ }
+ }
+
+ if (!xGlyph) {
+ xGlyph = this._getFirstGlyph();
+ }
+
+ var capGlyph = null;
+
+ for (i = 0; i < capChars.length; i++) {
+ var capChar = capChars[i];
+ capGlyph = this.glyphs[capChar.charCodeAt(0)];
+
+ if (capGlyph) {
+ break;
+ }
+ }
+
+ if (!capGlyph) {
+ for (var charCode in this.glyphs) {
+ if (this.glyphs.hasOwnProperty(charCode)) {
+ glyph = this.glyphs[charCode];
+
+ if (glyph.height === 0 || glyph.width === 0) {
+ continue;
+ }
+
+ this.capHeight = Math.max(this.capHeight, glyph.height);
+ }
+ }
+ } else {
+ this.capHeight = capGlyph.height;
+ }
+
+ this.capHeight -= padY;
+ }
+ });
+ })();
+
+ var howler = createCommonjsModule(function (module, exports) {
+ /*!
+ * howler.js v2.1.1
+ * howlerjs.com
+ *
+ * (c) 2013-2018, James Simpson of GoldFire Studios
+ * goldfirestudios.com
+ *
+ * MIT License
+ */
+
+ (function() {
+
+ /** Global Methods **/
+ /***************************************************************************/
+
+ /**
+ * Create the global controller. All contained methods and properties apply
+ * to all sounds that are currently playing or will be in the future.
+ */
+ var HowlerGlobal = function() {
+ this.init();
+ };
+ HowlerGlobal.prototype = {
+ /**
+ * Initialize the global Howler object.
+ * @return {Howler}
+ */
+ init: function() {
+ var self = this || Howler;
+
+ // Create a global ID counter.
+ self._counter = 1000;
+
+ // Pool of unlocked HTML5 Audio objects.
+ self._html5AudioPool = [];
+ self.html5PoolSize = 10;
+
+ // Internal properties.
+ self._codecs = {};
+ self._howls = [];
+ self._muted = false;
+ self._volume = 1;
+ self._canPlayEvent = 'canplaythrough';
+ self._navigator = (typeof window !== 'undefined' && window.navigator) ? window.navigator : null;
+
+ // Public properties.
+ self.masterGain = null;
+ self.noAudio = false;
+ self.usingWebAudio = true;
+ self.autoSuspend = true;
+ self.ctx = null;
+
+ // Set to false to disable the auto audio unlocker.
+ self.autoUnlock = true;
+
+ // Setup the various state values for global tracking.
+ self._setup();
+
+ return self;
+ },
+
+ /**
+ * Get/set the global volume for all sounds.
+ * @param {Float} vol Volume from 0.0 to 1.0.
+ * @return {Howler/Float} Returns self or current volume.
+ */
+ volume: function(vol) {
+ var self = this || Howler;
+ vol = parseFloat(vol);
+
+ // If we don't have an AudioContext created yet, run the setup.
+ if (!self.ctx) {
+ setupAudioContext();
+ }
+
+ if (typeof vol !== 'undefined' && vol >= 0 && vol <= 1) {
+ self._volume = vol;
+
+ // Don't update any of the nodes if we are muted.
+ if (self._muted) {
+ return self;
+ }
+
+ // When using Web Audio, we just need to adjust the master gain.
+ if (self.usingWebAudio) {
+ self.masterGain.gain.setValueAtTime(vol, Howler.ctx.currentTime);
+ }
+
+ // Loop through and change volume for all HTML5 audio nodes.
+ for (var i=0; i
+ * melonJS supports a wide array of audio codecs that have varying browser support :
+ * ("mp3", "mpeg", opus", "ogg", "oga", "wav", "aac", "caf", "m4a", "mp4", "weba", "webm", "dolby", "flac").
+ * For a maximum browser coverage the recommendation is to use at least two of them,
+ * typically default to webm and then fallback to mp3 for the best balance of small filesize and high quality,
+ * webm has nearly full browser coverage with a great combination of compression and quality, and mp3 will fallback gracefully for other browsers.
+ * It is important to remember that melonJS selects the first compatible sound based on the list of extensions and given order passed here.
+ * So if you want webm to be used before mp3, you need to put the audio format in that order.
+ * @name init
+ * @memberOf me.audio
+ * @public
+ * @function
+ * @param {String} [audioFormat="mp3"] audio format provided
+ * @return {Boolean} Indicates whether audio initialization was successful
+ * @example
+ * // initialize the "sound engine", giving "webm" as default desired audio format, and "mp3" as a fallback
+ * if (!me.audio.init("webm,mp3")) {
+ * alert("Sorry but your browser does not support html 5 audio !");
+ * return;
+ * }
+ */
+
+
+ api.init = function (audioFormat) {
+ if (!me.initialized) {
+ throw new Error("me.audio.init() called before engine initialization.");
+ } // if no param is given to init we use mp3 by default
+
+
+ audioFormat = typeof audioFormat === "string" ? audioFormat : "mp3"; // convert it into an array
+
+ this.audioFormats = audioFormat.split(",");
+ return !howler_1.noAudio;
+ };
+ /**
+ * return true if audio (HTML5 or WebAudio) is supported
+ * @see me.audio#hasAudio
+ * @name hasAudio
+ * @memberOf me.audio
+ * @public
+ * @function
+ */
+
+
+ api.hasAudio = function () {
+ return !howler_1.noAudio;
+ };
+ /**
+ * enable audio output
+ * only useful if audio supported and previously disabled through
+ *
+ * @see me.audio#disable
+ * @name enable
+ * @memberOf me.audio
+ * @public
+ * @function
+ */
+
+
+ api.enable = function () {
+ this.unmuteAll();
+ };
+ /**
+ * disable audio output
+ *
+ * @name disable
+ * @memberOf me.audio
+ * @public
+ * @function
+ */
+
+
+ api.disable = function () {
+ this.muteAll();
+ };
+ /**
+ * Load an audio file.
+ *
+ * sound item must contain the following fields :
+ * - name : name of the sound
+ * - src : source path
+ * @ignore
+ */
+
+
+ api.load = function (sound, html5, onload_cb, onerror_cb) {
+ var urls = [];
+
+ if (typeof this.audioFormats === "undefined" || this.audioFormats.length === 0) {
+ throw new Error("target audio extension(s) should be set through me.audio.init() before calling the preloader.");
+ }
+
+ for (var i = 0; i < this.audioFormats.length; i++) {
+ urls.push(sound.src + sound.name + "." + this.audioFormats[i] + me.loader.nocache);
+ }
+
+ audioTracks[sound.name] = new howler_2({
+ src: urls,
+ volume: howler_1.volume(),
+ html5: html5 === true,
+ xhrWithCredentials: me.loader.withCredentials,
+
+ /**
+ * @ignore
+ */
+ onloaderror: function onloaderror() {
+ soundLoadError.call(me.audio, sound.name, onerror_cb);
+ },
+
+ /**
+ * @ignore
+ */
+ onload: function onload() {
+ retry_counter = 0;
+
+ if (onload_cb) {
+ onload_cb();
+ }
+ }
+ });
+ return 1;
+ };
+ /**
+ * play the specified sound
+ * @name play
+ * @memberOf me.audio
+ * @public
+ * @function
+ * @param {String} sound_name audio clip name - case sensitive
+ * @param {Boolean} [loop=false] loop audio
+ * @param {Function} [onend] Function to call when sound instance ends playing.
+ * @param {Number} [volume=default] Float specifying volume (0.0 - 1.0 values accepted).
+ * @return {Number} the sound instance ID.
+ * @example
+ * // play the "cling" audio clip
+ * me.audio.play("cling");
+ * // play & repeat the "engine" audio clip
+ * me.audio.play("engine", true);
+ * // play the "gameover_sfx" audio clip and call myFunc when finished
+ * me.audio.play("gameover_sfx", false, myFunc);
+ * // play the "gameover_sfx" audio clip with a lower volume level
+ * me.audio.play("gameover_sfx", false, null, 0.5);
+ */
+
+
+ api.play = function (sound_name, loop, onend, volume) {
+ var sound = audioTracks[sound_name];
+
+ if (sound && typeof sound !== "undefined") {
+ var id = sound.play();
+
+ if (typeof loop === "boolean") {
+ // arg[0] can take different types in howler 2.0
+ sound.loop(loop, id);
+ }
+
+ sound.volume(typeof volume === "number" ? me.Math.clamp(volume, 0.0, 1.0) : howler_1.volume(), id);
+
+ if (typeof onend === "function") {
+ if (loop === true) {
+ sound.on("end", onend, id);
+ } else {
+ sound.once("end", onend, id);
+ }
+ }
+
+ return id;
+ } else {
+ throw new Error("audio clip " + sound_name + " does not exist");
+ }
+ };
+ /**
+ * Fade a currently playing sound between two volumee.
+ * @name fade
+ * @memberOf me.audio
+ * @public
+ * @function
+ * @param {String} sound_name audio clip name - case sensitive
+ * @param {Number} from Volume to fade from (0.0 to 1.0).
+ * @param {Number} to Volume to fade to (0.0 to 1.0).
+ * @param {Number} duration Time in milliseconds to fade.
+ * @param {Number} [id] the sound instance ID. If none is passed, all sounds in group will fade.
+ */
+
+
+ api.fade = function (sound_name, from, to, duration, id) {
+ var sound = audioTracks[sound_name];
+
+ if (sound && typeof sound !== "undefined") {
+ sound.fade(from, to, duration, id);
+ } else {
+ throw new Error("audio clip " + sound_name + " does not exist");
+ }
+ };
+ /**
+ * get/set the position of playback for a sound.
+ * @name seek
+ * @memberOf me.audio
+ * @public
+ * @function
+ * @param {String} sound_name audio clip name - case sensitive
+ * @param {Number} [seek] The position to move current playback to (in seconds).
+ * @param {Number} [id] the sound instance ID. If none is passed, all sounds in group will changed.
+ * @return return the current seek position (if no extra parameters were given)
+ * @example
+ * // return the current position of the background music
+ * var current_pos = me.audio.seek("dst-gameforest");
+ * // set back the position of the background music to the beginning
+ * me.audio.seek("dst-gameforest", 0);
+ */
+
+
+ api.seek = function (sound_name, seek, id) {
+ var sound = audioTracks[sound_name];
+
+ if (sound && typeof sound !== "undefined") {
+ return sound.seek.apply(sound, Array.prototype.slice.call(arguments, 1));
+ } else {
+ throw new Error("audio clip " + sound_name + " does not exist");
+ }
+ };
+ /**
+ * get or set the rate of playback for a sound.
+ * @name rate
+ * @memberOf me.audio
+ * @public
+ * @function
+ * @param {String} sound_name audio clip name - case sensitive
+ * @param {Number} [rate] playback rate : 0.5 to 4.0, with 1.0 being normal speed.
+ * @param {Number} [id] the sound instance ID. If none is passed, all sounds in group will be changed.
+ * @return return the current playback rate (if no extra parameters were given)
+ * @example
+ * // get the playback rate of the background music
+ * var rate = me.audio.rate("dst-gameforest");
+ * // speed up the playback of the background music
+ * me.audio.rate("dst-gameforest", 2.0);
+ */
+
+
+ api.rate = function (sound_name, rate, id) {
+ var sound = audioTracks[sound_name];
+
+ if (sound && typeof sound !== "undefined") {
+ return sound.rate.apply(sound, Array.prototype.slice.call(arguments, 1));
+ } else {
+ throw new Error("audio clip " + sound_name + " does not exist");
+ }
+ };
+ /**
+ * stop the specified sound on all channels
+ * @name stop
+ * @memberOf me.audio
+ * @public
+ * @function
+ * @param {String} sound_name audio clip name - case sensitive
+ * @param {Number} [id] the sound instance ID. If none is passed, all sounds in group will stop.
+ * @example
+ * me.audio.stop("cling");
+ */
+
+
+ api.stop = function (sound_name, id) {
+ var sound = audioTracks[sound_name];
+
+ if (sound && typeof sound !== "undefined") {
+ sound.stop(id); // remove the defined onend callback (if any defined)
+
+ sound.off("end", undefined, id);
+ } else {
+ throw new Error("audio clip " + sound_name + " does not exist");
+ }
+ };
+ /**
+ * pause the specified sound on all channels
+ * this function does not reset the currentTime property
+ * @name pause
+ * @memberOf me.audio
+ * @public
+ * @function
+ * @param {String} sound_name audio clip name - case sensitive
+ * @param {Number} [id] the sound instance ID. If none is passed, all sounds in group will pause.
+ * @example
+ * me.audio.pause("cling");
+ */
+
+
+ api.pause = function (sound_name, id) {
+ var sound = audioTracks[sound_name];
+
+ if (sound && typeof sound !== "undefined") {
+ sound.pause(id);
+ } else {
+ throw new Error("audio clip " + sound_name + " does not exist");
+ }
+ };
+ /**
+ * resume the specified sound on all channels
+ * @name resume
+ * @memberOf me.audio
+ * @public
+ * @function
+ * @param {String} sound_name audio clip name - case sensitive
+ * @param {Number} [id] the sound instance ID. If none is passed, all sounds in group will resume.
+ * @example
+ * // play a audio clip
+ * var id = me.audio.play("myClip");
+ * ...
+ * // pause it
+ * me.audio.pause("myClip", id);
+ * ...
+ * // resume
+ * me.audio.resume("myClip", id);
+ */
+
+
+ api.resume = function (sound_name, id) {
+ var sound = audioTracks[sound_name];
+
+ if (sound && typeof sound !== "undefined") {
+ sound.play(id);
+ } else {
+ throw new Error("audio clip " + sound_name + " does not exist");
+ }
+ };
+ /**
+ * play the specified audio track
+ * this function automatically set the loop property to true
+ * and keep track of the current sound being played.
+ * @name playTrack
+ * @memberOf me.audio
+ * @public
+ * @function
+ * @param {String} sound_name audio track name - case sensitive
+ * @param {Number} [volume=default] Float specifying volume (0.0 - 1.0 values accepted).
+ * @return {Number} the sound instance ID.
+ * @example
+ * me.audio.playTrack("awesome_music");
+ */
+
+
+ api.playTrack = function (sound_name, volume) {
+ current_track_id = sound_name;
+ return me.audio.play(current_track_id, true, null, volume);
+ };
+ /**
+ * stop the current audio track
+ *
+ * @see me.audio#playTrack
+ * @name stopTrack
+ * @memberOf me.audio
+ * @public
+ * @function
+ * @example
+ * // play a awesome music
+ * me.audio.playTrack("awesome_music");
+ * // stop the current music
+ * me.audio.stopTrack();
+ */
+
+
+ api.stopTrack = function () {
+ if (current_track_id !== null) {
+ audioTracks[current_track_id].stop();
+ current_track_id = null;
+ }
+ };
+ /**
+ * pause the current audio track
+ *
+ * @name pauseTrack
+ * @memberOf me.audio
+ * @public
+ * @function
+ * @example
+ * me.audio.pauseTrack();
+ */
+
+
+ api.pauseTrack = function () {
+ if (current_track_id !== null) {
+ audioTracks[current_track_id].pause();
+ }
+ };
+ /**
+ * resume the previously paused audio track
+ *
+ * @name resumeTrack
+ * @memberOf me.audio
+ * @public
+ * @function
+ * @example
+ * // play an awesome music
+ * me.audio.playTrack("awesome_music");
+ * // pause the audio track
+ * me.audio.pauseTrack();
+ * // resume the music
+ * me.audio.resumeTrack();
+ */
+
+
+ api.resumeTrack = function () {
+ if (current_track_id !== null) {
+ audioTracks[current_track_id].play();
+ }
+ };
+ /**
+ * returns the current track Id
+ * @name getCurrentTrack
+ * @memberOf me.audio
+ * @public
+ * @function
+ * @return {String} audio track name
+ */
+
+
+ api.getCurrentTrack = function () {
+ return current_track_id;
+ };
+ /**
+ * set the default global volume
+ * @name setVolume
+ * @memberOf me.audio
+ * @public
+ * @function
+ * @param {Number} volume Float specifying volume (0.0 - 1.0 values accepted).
+ */
+
+
+ api.setVolume = function (volume) {
+ howler_1.volume(volume);
+ };
+ /**
+ * get the default global volume
+ * @name getVolume
+ * @memberOf me.audio
+ * @public
+ * @function
+ * @returns {Number} current volume value in Float [0.0 - 1.0] .
+ */
+
+
+ api.getVolume = function () {
+ return howler_1.volume();
+ };
+ /**
+ * mute or unmute the specified sound, but does not pause the playback.
+ * @name mute
+ * @memberOf me.audio
+ * @public
+ * @function
+ * @param {String} sound_name audio clip name - case sensitive
+ * @param {Number} [id] the sound instance ID. If none is passed, all sounds in group will mute.
+ * @param {Boolean} [mute=true] True to mute and false to unmute
+ * @example
+ * // mute the background music
+ * me.audio.mute("awesome_music");
+ */
+
+
+ api.mute = function (sound_name, id, mute) {
+ // if not defined : true
+ mute = typeof mute === "undefined" ? true : !!mute;
+ var sound = audioTracks[sound_name];
+
+ if (sound && typeof sound !== "undefined") {
+ sound.mute(mute, id);
+ } else {
+ throw new Error("audio clip " + sound_name + " does not exist");
+ }
+ };
+ /**
+ * unmute the specified sound
+ * @name unmute
+ * @memberOf me.audio
+ * @public
+ * @function
+ * @param {String} sound_name audio clip name
+ * @param {Number} [id] the sound instance ID. If none is passed, all sounds in group will unmute.
+ */
+
+
+ api.unmute = function (sound_name, id) {
+ api.mute(sound_name, id, false);
+ };
+ /**
+ * mute all audio
+ * @name muteAll
+ * @memberOf me.audio
+ * @public
+ * @function
+ */
+
+
+ api.muteAll = function () {
+ howler_1.mute(true);
+ };
+ /**
+ * unmute all audio
+ * @name unmuteAll
+ * @memberOf me.audio
+ * @public
+ * @function
+ */
+
+
+ api.unmuteAll = function () {
+ howler_1.mute(false);
+ };
+ /**
+ * Returns true if audio is muted globally.
+ * @name muted
+ * @memberOf me.audio
+ * @public
+ * @function
+ * @return {Boolean} true if audio is muted globally
+ */
+
+
+ api.muted = function () {
+ return howler_1._muted;
+ };
+ /**
+ * unload specified audio track to free memory
+ *
+ * @name unload
+ * @memberOf me.audio
+ * @public
+ * @function
+ * @param {String} sound_name audio track name - case sensitive
+ * @return {Boolean} true if unloaded
+ * @example
+ * me.audio.unload("awesome_music");
+ */
+
+
+ api.unload = function (sound_name) {
+ if (!(sound_name in audioTracks)) {
+ return false;
+ } // destroy the Howl object
+
+
+ audioTracks[sound_name].unload();
+ delete audioTracks[sound_name];
+ return true;
+ };
+ /**
+ * unload all audio to free memory
+ *
+ * @name unloadAll
+ * @memberOf me.audio
+ * @public
+ * @function
+ * @example
+ * me.audio.unloadAll();
+ */
+
+
+ api.unloadAll = function () {
+ for (var sound_name in audioTracks) {
+ if (audioTracks.hasOwnProperty(sound_name)) {
+ api.unload(sound_name);
+ }
+ }
+ }; // return our object
+
+
+ return api;
+ }();
+ })();
+
+ (function () {
+ /**
+ * video functions
+ * There is no constructor function for me.video
+ * @namespace me.video
+ * @memberOf me
+ */
+ me.video = function () {
+ // hold public stuff in our api
+ var api = {};
+ var deferResizeId = 0;
+ var designRatio = 1;
+ var designWidth = 0;
+ var designHeight = 0; // max display size
+
+ var maxWidth = Infinity;
+ var maxHeight = Infinity; // default video settings
+
+ var settings = {
+ wrapper: undefined,
+ renderer: 2,
+ // AUTO
+ doubleBuffering: false,
+ autoScale: false,
+ scale: 1.0,
+ scaleMethod: "fit",
+ transparent: false,
+ blendMode: "normal",
+ antiAlias: false,
+ failIfMajorPerformanceCaveat: true,
+ subPixel: false,
+ verbose: false,
+ consoleHeader: true
+ };
+ /**
+ * Auto-detect the best renderer to use
+ * @ignore
+ */
+
+ function autoDetectRenderer(c, width, height, options) {
+ try {
+ return new me.WebGLRenderer(c, width, height, options);
+ } catch (e) {
+ return new me.CanvasRenderer(c, width, height, options);
+ }
+ }
+ /*
+ * PUBLIC STUFF
+ */
+
+ /**
+ * Select the HTML5 Canvas renderer
+ * @public
+ * @name CANVAS
+ * @memberOf me.video
+ * @enum {Number}
+ */
+
+
+ api.CANVAS = 0;
+ /**
+ * Select the WebGL renderer
+ * @public
+ * @name WEBGL
+ * @memberOf me.video
+ * @enum {Number}
+ */
+
+ api.WEBGL = 1;
+ /**
+ * Auto-select the renderer (Attempt WebGL first, with fallback to Canvas)
+ * @public
+ * @name AUTO
+ * @memberOf me.video
+ * @enum {Number}
+ */
+
+ api.AUTO = 2;
+ /**
+ * Initialize the "video" system (create a canvas based on the given arguments, and the related renderer).
+ * melonJS support various scaling mode, that can be enabled once the scale option is set to `auto` :
+ * - `fit` : Letterboxed; content is scaled to design aspect ratio
+ *
+ * - `fill-min` : Canvas is resized to fit minimum design resolution; content is scaled to design aspect ratio
+ *
+ * - `fill-max` : Canvas is resized to fit maximum design resolution; content is scaled to design aspect ratio
+ *
+ * - `flex`< : Canvas width & height is resized to fit; content is scaled to design aspect ratio
+ *
+ * - `flex-width` : Canvas width is resized to fit; content is scaled to design aspect ratio
+ *
+ * - `flex-height` : Canvas height is resized to fit; content is scaled to design aspect ratio
+ *
+ * - `stretch` : Canvas is resized to fit; content is scaled to screen aspect ratio
+ *
+ * @name init
+ * @memberOf me.video
+ * @function
+ * @param {Number} width the width of the canvas viewport
+ * @param {Number} height the height of the canvas viewport
+ * @param {Object} [options] The optional video/renderer parameters.
(see Renderer(s) documentation for further specific options)
+ * @param {String} [options.wrapper=document.body] the "div" element name to hold the canvas in the HTML file
+ * @param {Number} [options.renderer=me.video.AUTO] renderer to use.
+ * @param {Boolean} [options.doubleBuffering=false] enable/disable double buffering
+ * @param {Number|String} [options.scale=1.0] enable scaling of the canvas ('auto' for automatic scaling)
+ * @param {String} [options.scaleMethod="fit"] screen scaling modes ('fit','fill-min','fill-max','flex','flex-width','flex-height','stretch')
+ * @param {Boolean} [options.useParentDOMSize=false] on browser devices, limit the canvas width and height to its parent container dimensions as returned by getBoundingClientRect(),
+ * as opposed to the browser window dimensions
+ * @param {Boolean} [options.transparent=false] whether to allow transparent pixels in the front buffer (screen)
+ * @param {Boolean} [options.antiAlias=false] whether to enable or not video scaling interpolation
+ * @param {Boolean} [options.consoleHeader=true] whether to display melonJS version and basic device information in the console
+ * @return {Boolean} false if initialization failed (canvas not supported)
+ * @see me.CanvasRenderer
+ * @see me.WebGLRenderer
+ * @example
+ * // init the video with a 640x480 canvas
+ * me.video.init(640, 480, {
+ * wrapper : "screen",
+ * renderer : me.video.AUTO,
+ * scale : "auto",
+ * scaleMethod : "fit",
+ * doubleBuffering : true
+ * });
+ */
+
+ api.init = function (game_width, game_height, options) {
+ // ensure melonjs has been properly initialized
+ if (!me.initialized) {
+ throw new Error("me.video.init() called before engine initialization.");
+ } // revert to default options if not defined
+
+
+ settings = Object.assign(settings, options || {}); // sanitize potential given parameters
+
+ settings.doubleBuffering = !!settings.doubleBuffering;
+ settings.useParentDOMSize = !!settings.useParentDOMSize;
+ settings.autoScale = settings.scale === "auto" || false;
+ settings.transparent = !!settings.transparent;
+ settings.antiAlias = !!settings.antiAlias;
+ settings.failIfMajorPerformanceCaveat = !!settings.failIfMajorPerformanceCaveat;
+ settings.subPixel = !!settings.subPixel;
+ settings.verbose = !!settings.verbose;
+
+ if (settings.scaleMethod.search(/^(fill-(min|max)|fit|flex(-(width|height))?|stretch)$/) !== 0) {
+ settings.scaleMethod = "fit";
+ } // override renderer settings if &webgl is defined in the URL
+
+
+ if (me.game.HASH.webgl === true) {
+ settings.renderer = api.WEBGL;
+ } // normalize scale
+
+
+ settings.scale = settings.autoScale ? 1.0 : +settings.scale || 1.0;
+ me.sys.scale = new me.Vector2d(settings.scale, settings.scale); // force double buffering if scaling is required
+
+ if (settings.autoScale || settings.scale !== 1.0) {
+ settings.doubleBuffering = true;
+ } // hold the requested video size ratio
+
+
+ designRatio = game_width / game_height;
+ designWidth = game_width;
+ designHeight = game_height; // default scaled size value
+
+ var game_width_zoom = game_width * me.sys.scale.x;
+ var game_height_zoom = game_height * me.sys.scale.y;
+ settings.zoomX = game_width_zoom;
+ settings.zoomY = game_height_zoom; //add a channel for the onresize/onorientationchange event
+
+ window.addEventListener("resize", me.utils.function.throttle(function (event) {
+ me.event.publish(me.event.WINDOW_ONRESIZE, [event]);
+ }, 100), false); // Screen Orientation API
+
+ window.addEventListener("orientationchange", function (event) {
+ me.event.publish(me.event.WINDOW_ONORIENTATION_CHANGE, [event]);
+ }, false); // pre-fixed implementation on mozzila
+
+ window.addEventListener("onmozorientationchange", function (event) {
+ me.event.publish(me.event.WINDOW_ONORIENTATION_CHANGE, [event]);
+ }, false);
+
+ if (typeof window.screen !== "undefined") {
+ // is this one required ?
+ window.screen.onorientationchange = function (event) {
+ me.event.publish(me.event.WINDOW_ONORIENTATION_CHANGE, [event]);
+ };
+ } // Automatically update relative canvas position on scroll
+
+
+ window.addEventListener("scroll", me.utils.function.throttle(function (e) {
+ me.video.renderer.updateBounds();
+ me.event.publish(me.event.WINDOW_ONSCROLL, [e]);
+ }, 100), false); // register to the channel
+
+ me.event.subscribe(me.event.WINDOW_ONRESIZE, me.video.onresize.bind(me.video));
+ me.event.subscribe(me.event.WINDOW_ONORIENTATION_CHANGE, me.video.onresize.bind(me.video)); // create the main screen canvas
+
+ var canvas;
+
+ if (me.device.ejecta === true) {
+ // a main canvas is already automatically created by Ejecta
+ canvas = document.getElementById("canvas");
+ } else if (typeof window.canvas !== "undefined") {
+ // a global canvas is available, e.g. webapp adapter for wechat
+ canvas = window.canvas;
+ } else {
+ canvas = api.createCanvas(game_width_zoom, game_height_zoom);
+ } // add our canvas
+
+
+ if (options.wrapper) {
+ settings.wrapper = document.getElementById(options.wrapper);
+ } // if wrapperid is not defined (null)
+
+
+ if (!settings.wrapper) {
+ // add the canvas to document.body
+ settings.wrapper = document.body;
+ }
+
+ settings.wrapper.appendChild(canvas); // stop here if not supported
+
+ if (typeof canvas.getContext === "undefined") {
+ return false;
+ }
+ /**
+ * A reference to the current video renderer
+ * @public
+ * @memberOf me.video
+ * @name renderer
+ * @type {me.Renderer|me.CanvasRenderer|me.WebGLRenderer}
+ */
+
+
+ switch (settings.renderer) {
+ case api.AUTO:
+ case api.WEBGL:
+ this.renderer = autoDetectRenderer(canvas, game_width, game_height, settings);
+ break;
+
+ default:
+ this.renderer = new me.CanvasRenderer(canvas, game_width, game_height, settings);
+ break;
+ } // adjust CSS style for High-DPI devices
+
+
+ var ratio = me.device.devicePixelRatio;
+
+ if (ratio > 1) {
+ canvas.style.width = canvas.width / ratio + "px";
+ canvas.style.height = canvas.height / ratio + "px";
+ } // set max the canvas max size if CSS values are defined
+
+
+ if (window.getComputedStyle) {
+ var style = window.getComputedStyle(canvas, null);
+ me.video.setMaxSize(parseInt(style.maxWidth, 10), parseInt(style.maxHeight, 10));
+ }
+
+ me.game.init(); // trigger an initial resize();
+
+ me.video.onresize(); // add an observer to detect when the dom tree is modified
+
+ if ("MutationObserver" in window) {
+ // Create an observer instance linked to the callback function
+ var observer = new MutationObserver(me.video.onresize.bind(me.video)); // Start observing the target node for configured mutations
+
+ observer.observe(settings.wrapper, {
+ attributes: false,
+ childList: true,
+ subtree: true
+ });
+ }
+
+ if (options.consoleHeader !== false) {
+ var renderType = me.video.renderer instanceof me.CanvasRenderer ? "CANVAS" : "WebGL";
+ var audioType = me.device.hasWebAudio ? "Web Audio" : "HTML5 Audio"; // output video information in the console
+
+ console.log(me.mod + " " + me.version + " | http://melonjs.org");
+ console.log(renderType + " | " + audioType + " | " + "pixel ratio " + me.device.devicePixelRatio + " | " + (me.device.isMobile ? "mobile" : "desktop") + " | " + me.device.getScreenOrientation() + " | " + me.device.language);
+ console.log("resolution: " + "requested " + game_width + "x" + game_height + ", got " + me.video.renderer.getWidth() + "x" + me.video.renderer.getHeight());
+ }
+
+ return true;
+ };
+ /**
+ * set the max canvas display size (when scaling)
+ * @name setMaxSize
+ * @memberOf me.video
+ * @function
+ * @param {Number} width width
+ * @param {Number} height height
+ */
+
+
+ api.setMaxSize = function (w, h) {
+ // max display size
+ maxWidth = w || Infinity;
+ maxHeight = h || Infinity; // trigger a resize
+ // defer it to ensure everything is properly intialized
+
+ me.utils.function.defer(me.video.onresize, me.video);
+ };
+ /**
+ * Create and return a new Canvas
+ * @name createCanvas
+ * @memberOf me.video
+ * @function
+ * @param {Number} width width
+ * @param {Number} height height
+ * @return {Canvas}
+ */
+
+
+ api.createCanvas = function (width, height) {
+ if (width === 0 || height === 0) {
+ throw new Error("width or height was zero, Canvas could not be initialized !");
+ }
+
+ var _canvas = document.createElement("canvas");
+
+ _canvas.width = width;
+ _canvas.height = height;
+ return _canvas;
+ };
+ /**
+ * return a reference to the wrapper
+ * @name getWrapper
+ * @memberOf me.video
+ * @function
+ * @return {Document}
+ */
+
+
+ api.getWrapper = function () {
+ return settings.wrapper;
+ };
+ /**
+ * callback for window resize event
+ * @ignore
+ */
+
+
+ api.onresize = function () {
+ // default (no scaling)
+ var scaleX = 1,
+ scaleY = 1;
+
+ if (settings.autoScale) {
+ var parentNodeWidth;
+ var parentNodeHeight;
+ var parentNode = me.video.renderer.getScreenCanvas().parentNode;
+
+ if (typeof parentNode !== "undefined") {
+ if (settings.useParentDOMSize && typeof parentNode.getBoundingClientRect === "function") {
+ var rect = parentNode.getBoundingClientRect();
+ parentNodeWidth = rect.width || rect.right - rect.left;
+ parentNodeHeight = rect.height || rect.bottom - rect.top;
+ } else {
+ // for cased where DOM is not implemented and so parentNode (e.g. Ejecta, Weixin)
+ parentNodeWidth = parentNode.width;
+ parentNodeHeight = parentNode.height;
+ }
+ }
+
+ var _max_width = Math.min(maxWidth, parentNodeWidth || window.innerWidth);
+
+ var _max_height = Math.min(maxHeight, parentNodeHeight || window.innerHeight);
+
+ var screenRatio = _max_width / _max_height;
+ var sWidth = Infinity;
+ var sHeight = Infinity;
+
+ if (settings.scaleMethod === "fill-min" && screenRatio > designRatio || settings.scaleMethod === "fill-max" && screenRatio < designRatio || settings.scaleMethod === "flex-width") {
+ // resize the display canvas to fill the parent container
+ sWidth = Math.min(maxWidth, designHeight * screenRatio);
+ scaleX = scaleY = _max_width / sWidth;
+ sWidth = ~~(sWidth + 0.5);
+ this.renderer.resize(sWidth, designHeight);
+ } else if (settings.scaleMethod === "fill-min" && screenRatio < designRatio || settings.scaleMethod === "fill-max" && screenRatio > designRatio || settings.scaleMethod === "flex-height") {
+ // resize the display canvas to fill the parent container
+ sHeight = Math.min(maxHeight, designWidth * (_max_height / _max_width));
+ scaleX = scaleY = _max_height / sHeight;
+ sHeight = ~~(sHeight + 0.5);
+ this.renderer.resize(designWidth, sHeight);
+ } else if (settings.scaleMethod === "flex") {
+ // resize the display canvas to fill the parent container
+ this.renderer.resize(_max_width, _max_height);
+ } else if (settings.scaleMethod === "stretch") {
+ // scale the display canvas to fit with the parent container
+ scaleX = _max_width / designWidth;
+ scaleY = _max_height / designHeight;
+ } else {
+ // scale the display canvas to fit the parent container
+ // make sure we maintain the original aspect ratio
+ if (screenRatio < designRatio) {
+ scaleX = scaleY = _max_width / designWidth;
+ } else {
+ scaleX = scaleY = _max_height / designHeight;
+ }
+ } // adjust scaling ratio based on the device pixel ratio
+
+
+ scaleX *= me.device.devicePixelRatio;
+ scaleY *= me.device.devicePixelRatio;
+
+ if (deferResizeId) {
+ // cancel any previous pending resize
+ clearTimeout(deferResizeId);
+ }
+
+ deferResizeId = me.utils.function.defer(me.video.updateDisplaySize, this, scaleX, scaleY);
+ } // update parent container bounds
+
+
+ this.renderer.updateBounds();
+ };
+ /**
+ * Modify the "displayed" canvas size
+ * @name updateDisplaySize
+ * @memberOf me.video
+ * @function
+ * @param {Number} scaleX X scaling multiplier
+ * @param {Number} scaleY Y scaling multiplier
+ */
+
+
+ api.updateDisplaySize = function (scaleX, scaleY) {
+ // update the global scale variable
+ me.sys.scale.set(scaleX, scaleY); // renderer resize logic
+
+ this.renderer.scaleCanvas(scaleX, scaleY); // update parent container bounds
+
+ this.renderer.updateBounds(); // force repaint
+
+ me.game.repaint(); // clear the timeout id
+
+ deferResizeId = 0;
+ }; // return our api
+
+
+ return api;
+ }();
+ })();
+
+ (function () {
+ /**
+ * a base renderer object
+ * @class
+ * @extends me.Object
+ * @memberOf me
+ * @constructor
+ * @param {HTMLCanvasElement} canvas The html canvas tag to draw to on screen.
+ * @param {Number} width The width of the canvas without scaling
+ * @param {Number} height The height of the canvas without scaling
+ * @param {Object} [options] The renderer parameters
+ * @param {Boolean} [options.doubleBuffering=false] Whether to enable double buffering
+ * @param {Boolean} [options.antiAlias=false] Whether to enable anti-aliasing, use false (default) for a pixelated effect.
+ * @param {Boolean} [options.failIfMajorPerformanceCaveat=true] If true, the renderer will switch to CANVAS mode if the performances of a WebGL context would be dramatically lower than that of a native application making equivalent OpenGL calls.
+ * @param {Boolean} [options.transparent=false] Whether to enable transparency on the canvas (performance hit when enabled)
+ * @param {Boolean} [options.blendMode="normal"] the default blend mode to use ("normal", "multiply")
+ * @param {Boolean} [options.subPixel=false] Whether to enable subpixel rendering (performance hit when enabled)
+ * @param {Boolean} [options.verbose=false] Enable the verbose mode that provides additional details as to what the renderer is doing
+ * @param {Number} [options.zoomX=width] The actual width of the canvas with scaling applied
+ * @param {Number} [options.zoomY=height] The actual height of the canvas with scaling applied
+ */
+ me.Renderer = me.Object.extend({
+ /**
+ * @ignore
+ */
+ init: function init(c, width, height, options) {
+ /**
+ * The given constructor options
+ * @public
+ * @name settings
+ * @memberOf me.Renderer#
+ * @enum {Object}
+ */
+ this.settings = options;
+ /**
+ * true if the current rendering context is valid
+ * @name isContextValid
+ * @memberOf me.Renderer
+ * @default true
+ * type {Boolean}
+ */
+
+ this.isContextValid = true;
+ /**
+ * @ignore
+ */
+
+ this.currentScissor = new Int32Array([0, 0, this.width, this.height]);
+ /**
+ * @ignore
+ */
+
+ this.currentBlendMode = "normal"; // canvas size after scaling
+
+ this.gameWidthZoom = this.settings.zoomX || width;
+ this.gameHeightZoom = this.settings.zoomY || height; // canvas object and context
+
+ this.canvas = this.backBufferCanvas = c;
+ this.context = null; // global color
+
+ this.currentColor = new me.Color(0, 0, 0, 1.0); // global tint color
+
+ this.currentTint = new me.Color(255, 255, 255, 1.0); // default uvOffset
+
+ this.uvOffset = 0; // the parent container bouds
+
+ this.parentBounds = new me.Rect(0, 0, 0, 0); // reset the instantiated renderer on game reset
+
+ me.event.subscribe(me.event.GAME_RESET, function () {
+ me.video.renderer.reset();
+ });
+ return this;
+ },
+
+ /**
+ * prepare the framebuffer for drawing a new frame
+ * @name clear
+ * @memberOf me.Renderer.prototype
+ * @function
+ */
+ clear: function clear() {},
+
+ /**
+ * Reset context state
+ * @name reset
+ * @memberOf me.Renderer.prototype
+ * @function
+ */
+ reset: function reset() {
+ this.resetTransform();
+ this.setBlendMode(this.settings.blendMode);
+ this.setColor("#000000");
+ this.currentTint.setColor(255, 255, 255, 1.0);
+ this.cache.clear();
+ this.currentScissor[0] = 0;
+ this.currentScissor[1] = 0;
+ this.currentScissor[2] = this.backBufferCanvas.width;
+ this.currentScissor[3] = this.backBufferCanvas.height;
+ this.updateBounds();
+ },
+
+ /**
+ * update the bounds (size and position) of the parent container.
+ * (this can be manually called in case of manual page layout modification not triggering a resize event)
+ * @name updateBounds
+ * @memberOf me.Renderer.prototype
+ * @function
+ */
+ updateBounds: function updateBounds() {
+ var target = this.getScreenCanvas();
+ var rect;
+
+ if (typeof target.getBoundingClientRect === "undefined") {
+ rect = {
+ left: 0,
+ top: 0,
+ width: 0,
+ height: 0
+ };
+ } else {
+ rect = target.getBoundingClientRect();
+ }
+
+ this.parentBounds.setShape(rect.left, rect.top, rect.width, rect.height);
+ },
+
+ /**
+ * returns the bounds (size and position) of the parent container
+ * @name getBounds
+ * @memberOf me.Renderer.prototype
+ * @function
+ * @return {me.Rect}
+ */
+ getBounds: function getBounds() {
+ return this.parentBounds;
+ },
+
+ /**
+ * return a reference to the system canvas
+ * @name getCanvas
+ * @memberOf me.Renderer.prototype
+ * @function
+ * @return {HTMLCanvasElement}
+ */
+ getCanvas: function getCanvas() {
+ return this.backBufferCanvas;
+ },
+
+ /**
+ * return a reference to the screen canvas
+ * @name getScreenCanvas
+ * @memberOf me.Renderer.prototype
+ * @function
+ * @return {HTMLCanvasElement}
+ */
+ getScreenCanvas: function getScreenCanvas() {
+ return this.canvas;
+ },
+
+ /**
+ * return a reference to the screen canvas corresponding 2d Context
+ * (will return buffered context if double buffering is enabled, or a reference to the Screen Context)
+ * @name getScreenContext
+ * @memberOf me.Renderer.prototype
+ * @function
+ * @return {Context2d}
+ */
+ getScreenContext: function getScreenContext() {
+ return this.context;
+ },
+
+ /**
+ * returns the current blend mode for this renderer
+ * @name getBlendMode
+ * @memberOf me.Renderer.prototype
+ * @function
+ * @return {String} blend mode
+ */
+ getBlendMode: function getBlendMode() {
+ return this.currentBlendMode;
+ },
+
+ /**
+ * Returns the 2D Context object of the given Canvas
+ * Also configures anti-aliasing and blend modes based on constructor options.
+ * @name getContext2d
+ * @memberOf me.Renderer.prototype
+ * @function
+ * @param {HTMLCanvasElement} canvas
+ * @param {Boolean} [transparent=true] use false to disable transparency
+ * @return {Context2d}
+ */
+ getContext2d: function getContext2d(c, transparent) {
+ if (typeof c === "undefined" || c === null) {
+ throw new Error("You must pass a canvas element in order to create " + "a 2d context");
+ }
+
+ if (typeof c.getContext === "undefined") {
+ throw new Error("Your browser does not support HTML5 canvas.");
+ }
+
+ if (typeof transparent !== "boolean") {
+ transparent = true;
+ }
+
+ var _context = c.getContext("2d", {
+ "alpha": transparent
+ });
+
+ if (!_context.canvas) {
+ _context.canvas = c;
+ }
+
+ this.setAntiAlias(_context, this.settings.antiAlias);
+ return _context;
+ },
+
+ /**
+ * return the width of the system Canvas
+ * @name getWidth
+ * @memberOf me.Renderer.prototype
+ * @function
+ * @return {Number}
+ */
+ getWidth: function getWidth() {
+ return this.backBufferCanvas.width;
+ },
+
+ /**
+ * return the height of the system Canvas
+ * @name getHeight
+ * @memberOf me.Renderer.prototype
+ * @function
+ * @return {Number}
+ */
+ getHeight: function getHeight() {
+ return this.backBufferCanvas.height;
+ },
+
+ /**
+ * get the current fill & stroke style color.
+ * @name getColor
+ * @memberOf me.Renderer.prototype
+ * @function
+ * @param {me.Color} current global color
+ */
+ getColor: function getColor() {
+ return this.currentColor;
+ },
+
+ /**
+ * return the current global alpha
+ * @name globalAlpha
+ * @memberOf me.Renderer.prototype
+ * @function
+ * @return {Number}
+ */
+ globalAlpha: function globalAlpha() {
+ return this.currentColor.glArray[3];
+ },
+
+ /**
+ * check if the given rectangle overlaps with the renderer screen coordinates
+ * @name overlaps
+ * @memberOf me.Renderer.prototype
+ * @function
+ * @param {me.Rect} rect
+ * @return {boolean} true if overlaps
+ */
+ overlaps: function overlaps(rect) {
+ return rect.left < this.getWidth() && rect.right > 0 && rect.top < this.getHeight() && rect.bottom > 0;
+ },
+
+ /**
+ * resizes the system canvas
+ * @name resize
+ * @memberOf me.Renderer.prototype
+ * @function
+ * @param {Number} width new width of the canvas
+ * @param {Number} height new height of the canvas
+ */
+ resize: function resize(width, height) {
+ if (width !== this.backBufferCanvas.width || height !== this.backBufferCanvas.height) {
+ this.backBufferCanvas.width = width;
+ this.backBufferCanvas.height = height;
+ this.currentScissor[0] = 0;
+ this.currentScissor[1] = 0;
+ this.currentScissor[2] = width;
+ this.currentScissor[3] = height; // publish the corresponding event
+
+ me.event.publish(me.event.CANVAS_ONRESIZE, [width, height]);
+ }
+
+ this.updateBounds();
+ },
+
+ /**
+ * enable/disable image smoothing (scaling interpolation) for the given context
+ * @name setAntiAlias
+ * @memberOf me.Renderer.prototype
+ * @function
+ * @param {Context2d} context
+ * @param {Boolean} [enable=false]
+ */
+ setAntiAlias: function setAntiAlias(context, enable) {
+ var canvas = context.canvas; // enable/disable antialis on the given Context2d object
+
+ me.agent.setPrefixed("imageSmoothingEnabled", enable === true, context); // set antialias CSS property on the main canvas
+
+ if (enable !== true) {
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering
+ canvas.style["image-rendering"] = "pixelated";
+ canvas.style["image-rendering"] = "crisp-edges";
+ canvas.style["image-rendering"] = "-moz-crisp-edges";
+ canvas.style["image-rendering"] = "-o-crisp-edges";
+ canvas.style["image-rendering"] = "-webkit-optimize-contrast";
+ canvas.style.msInterpolationMode = "nearest-neighbor";
+ } else {
+ canvas.style["image-rendering"] = "auto";
+ }
+ },
+
+ /**
+ * stroke the given shape
+ * @name stroke
+ * @memberOf me.Renderer.prototype
+ * @function
+ * @param {me.Rect|me.Polygon|me.Line|me.Ellipse} shape a shape object to stroke
+ */
+ stroke: function stroke(shape, fill) {
+ if (shape.shapeType === "Rectangle") {
+ this.strokeRect(shape.left, shape.top, shape.width, shape.height, fill);
+ } else if (shape instanceof me.Line || shape instanceof me.Polygon) {
+ this.strokePolygon(shape, fill);
+ } else if (shape instanceof me.Ellipse) {
+ this.strokeEllipse(shape.pos.x, shape.pos.y, shape.radiusV.x, shape.radiusV.y, fill);
+ }
+ },
+
+ /**
+ * fill the given shape
+ * @name fill
+ * @memberOf me.Renderer.prototype
+ * @function
+ * @param {me.Rect|me.Polygon|me.Line|me.Ellipse} shape a shape object to fill
+ */
+ fill: function fill(shape) {
+ this.stroke(shape, true);
+ },
+
+ /**
+ * A mask limits rendering elements to the shape and position of the given mask object.
+ * So, if the renderable is larger than the mask, only the intersecting part of the renderable will be visible.
+ * Mask are not preserved through renderer context save and restore.
+ * @name setMask
+ * @memberOf me.Renderer.prototype
+ * @function
+ * @param {me.Rect|me.Polygon|me.Line|me.Ellipse} [mask] the shape defining the mask to be applied
+ */
+ setMask: function setMask(mask) {},
+
+ /**
+ * disable (remove) the rendering mask set through setMask.
+ * @name clearMask
+ * @see setMask
+ * @memberOf me.Renderer.prototype
+ * @function
+ */
+ clearMask: function clearMask() {},
+
+ /**
+ * set a rendering tint (WebGL only) for sprite based renderables.
+ * @name setTint
+ * @memberOf me.Renderer.prototype
+ * @function
+ * @param {me.Color} [tint] the tint color
+ */
+ setTint: function setTint(tint) {
+ // global tint color
+ this.currentTint.copy(tint);
+ },
+
+ /**
+ * clear the rendering tint set through setTint.
+ * @name clearTint
+ * @see setTint
+ * @memberOf me.Renderer.prototype
+ * @function
+ */
+ clearTint: function clearTint() {
+ // reset to default
+ this.currentTint.setColor(255, 255, 255, 1.0);
+ },
+
+ /**
+ * @ignore
+ */
+ drawFont: function drawFont()
+ /*bounds*/
+ {}
+ });
+ })();
+
+ (function () {
+ /**
+ * A Texture atlas object, currently supports :
+ * - [TexturePacker]{@link http://www.codeandweb.com/texturepacker/} : through JSON export (standard and multipack texture atlas)
+ * - [ShoeBox]{@link http://renderhjs.net/shoebox/} : through JSON export using the
+ * melonJS setting [file]{@link https://github.com/melonjs/melonJS/raw/master/media/shoebox_JSON_export.sbx}
+ * - Standard (fixed cell size) spritesheet : through a {framewidth:xx, frameheight:xx, anchorPoint:me.Vector2d} object
+ * @class
+ * @extends me.Object
+ * @memberOf me.Renderer
+ * @name Texture
+ * @constructor
+ * @param {Object|Object[]} atlas atlas information. See {@link me.loader.getJSON}
+ * @param {HTMLImageElement|HTMLCanvasElement|String|HTMLImageElement[]|HTMLCanvasElement[]|String[]} [source=atlas.meta.image] Image source
+ * @param {Boolean} [cached=false] Use true to skip caching this Texture
+ * @example
+ * // create a texture atlas from a JSON Object
+ * game.texture = new me.video.renderer.Texture(
+ * me.loader.getJSON("texture")
+ * );
+ *
+ * // create a texture atlas from a multipack JSON Object
+ * game.texture = new me.video.renderer.Texture([
+ * me.loader.getJSON("texture-0"),
+ * me.loader.getJSON("texture-1"),
+ * me.loader.getJSON("texture-2")
+ * ]);
+ *
+ * // create a texture atlas for a spritesheet with an anchorPoint in the center of each frame
+ * game.texture = new me.video.renderer.Texture(
+ * {
+ * framewidth : 32,
+ * frameheight : 32,
+ * anchorPoint : new me.Vector2d(0.5, 0.5)
+ * },
+ * me.loader.getImage("spritesheet")
+ * );
+ */
+ me.Renderer.prototype.Texture = me.Object.extend({
+ /**
+ * @ignore
+ */
+ init: function init(atlases, src, cache) {
+ /**
+ * to identify the atlas format (e.g. texture packer)
+ * @ignore
+ */
+ this.format = null;
+ /**
+ * the texture source(s) itself
+ * @type Map
+ * @ignore
+ */
+
+ this.sources = new Map();
+ /**
+ * the atlas dictionnaries
+ * @type Map
+ * @ignore
+ */
+
+ this.atlases = new Map(); // parse given atlas(es) paremeters
+
+ if (typeof atlases !== "undefined") {
+ // normalize to array to keep the following code generic
+ atlases = Array.isArray(atlases) ? atlases : [atlases];
+
+ for (var i in atlases) {
+ var atlas = atlases[i];
+
+ if (typeof atlas.meta !== "undefined") {
+ // Texture Packer
+ if (atlas.meta.app.includes("texturepacker")) {
+ this.format = "texturepacker"; // set the texture
+
+ if (typeof src === "undefined") {
+ // get the texture name from the atlas meta data
+ var image = me.loader.getImage(atlas.meta.image);
+
+ if (!image) {
+ throw new Error("Atlas texture '" + image + "' not found");
+ }
+
+ this.sources.set(atlas.meta.image, image);
+ } else {
+ this.sources.set(atlas.meta.image || "default", typeof src === "string" ? me.loader.getImage(src) : src);
+ }
+
+ this.repeat = "no-repeat";
+ } // ShoeBox
+ else if (atlas.meta.app.includes("ShoeBox")) {
+ if (!atlas.meta.exporter || !atlas.meta.exporter.includes("melonJS")) {
+ throw new Error("ShoeBox requires the JSON exporter : " + "https://github.com/melonjs/melonJS/tree/master/media/shoebox_JSON_export.sbx");
+ }
+
+ this.format = "ShoeBox";
+ this.repeat = "no-repeat";
+ this.sources.set("default", typeof src === "string" ? me.loader.getImage(src) : src);
+ } // Internal texture atlas
+ else if (atlas.meta.app.includes("melonJS")) {
+ this.format = "melonJS";
+ this.repeat = atlas.meta.repeat || "no-repeat";
+ this.sources.set("default", typeof src === "string" ? me.loader.getImage(src) : src);
+ } // initialize the atlas
+
+
+ this.atlases.set(atlas.meta.image || "default", this.parse(atlas));
+ } else {
+ // a regular spritesheet
+ if (typeof atlas.framewidth !== "undefined" && typeof atlas.frameheight !== "undefined") {
+ this.format = "Spritesheet (fixed cell size)";
+ this.repeat = "no-repeat";
+
+ if (typeof src !== "undefined") {
+ // overwrite if specified
+ atlas.image = typeof src === "string" ? me.loader.getImage(src) : src;
+ } // initialize the atlas
+
+
+ this.atlases.set("default", this.parseFromSpriteSheet(atlas));
+ this.sources.set("default", atlas.image);
+ }
+ }
+ } // end forEach
+
+ } // if format not recognized
+
+
+ if (this.atlases.length === 0) {
+ throw new Error("texture atlas format not supported");
+ } // Add self to TextureCache if cache !== false
+
+
+ if (cache !== false) {
+ src = Array.isArray(src) ? src : [src];
+ var _iteratorNormalCompletion = true;
+ var _didIteratorError = false;
+ var _iteratorError = undefined;
+
+ try {
+ for (var _iterator = this.sources[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
+ var source = _step.value;
+
+ if (cache instanceof me.Renderer.TextureCache) {
+ cache.set(source, this);
+ } else {
+ me.video.renderer.cache.set(source, this);
+ }
+ }
+ } catch (err) {
+ _didIteratorError = true;
+ _iteratorError = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion && _iterator.return != null) {
+ _iterator.return();
+ }
+ } finally {
+ if (_didIteratorError) {
+ throw _iteratorError;
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * create a simple 1 frame texture atlas based on the given parameters
+ * @ignore
+ */
+ createAtlas: function createAtlas(width, height, name, repeat) {
+ return {
+ "meta": {
+ "app": "melonJS",
+ "size": {
+ "w": width,
+ "h": height
+ },
+ "repeat": repeat || "no-repeat",
+ "image": "default"
+ },
+ "frames": [{
+ "filename": name || "default",
+ "frame": {
+ "x": 0,
+ "y": 0,
+ "w": width,
+ "h": height
+ }
+ }]
+ };
+ },
+
+ /**
+ * build an atlas from the given data
+ * @ignore
+ */
+ parse: function parse(data) {
+ var atlas = {};
+ var self = this;
+ data.frames.forEach(function (frame) {
+ // fix wrongly formatted JSON (e.g. last dummy object in ShoeBox)
+ if (frame.hasOwnProperty("filename")) {
+ // Source coordinates
+ var s = frame.frame;
+ var originX, originY; // Pixel-based offset origin from the top-left of the source frame
+
+ var hasTextureAnchorPoint = frame.spriteSourceSize && frame.sourceSize && frame.pivot;
+
+ if (hasTextureAnchorPoint) {
+ originX = frame.sourceSize.w * frame.pivot.x - (frame.trimmed ? frame.spriteSourceSize.x : 0);
+ originY = frame.sourceSize.h * frame.pivot.y - (frame.trimmed ? frame.spriteSourceSize.y : 0);
+ }
+
+ atlas[frame.filename] = {
+ name: frame.filename,
+ // frame name
+ texture: data.meta.image || "default",
+ // the source texture
+ offset: new me.Vector2d(s.x, s.y),
+ anchorPoint: hasTextureAnchorPoint ? new me.Vector2d(originX / s.w, originY / s.h) : null,
+ trimmed: !!frame.trimmed,
+ width: s.w,
+ height: s.h,
+ angle: frame.rotated === true ? -me.Math.ETA : 0
+ };
+ self.addUvsMap(atlas, frame.filename, data.meta.size.w, data.meta.size.h);
+ }
+ });
+ return atlas;
+ },
+
+ /**
+ * build an atlas from the given spritesheet
+ * @ignore
+ */
+ parseFromSpriteSheet: function parseFromSpriteSheet(data) {
+ var atlas = {};
+ var image = data.image;
+ var spacing = data.spacing || 0;
+ var margin = data.margin || 0;
+ var width = image.width;
+ var height = image.height; // calculate the sprite count (line, col)
+
+ var spritecount = me.pool.pull("me.Vector2d", ~~((width - margin + spacing) / (data.framewidth + spacing)), ~~((height - margin + spacing) / (data.frameheight + spacing))); // verifying the texture size
+
+ if (width % (data.framewidth + spacing) !== 0 || height % (data.frameheight + spacing) !== 0) {
+ // "truncate size"
+ width = spritecount.x * (data.framewidth + spacing);
+ height = spritecount.y * (data.frameheight + spacing); // warning message
+
+ console.warn("Spritesheet Texture for image: " + image.src + " is not divisible by " + (data.framewidth + spacing) + "x" + (data.frameheight + spacing) + ", truncating effective size to " + width + "x" + height);
+ } // build the local atlas
+
+
+ for (var frame = 0, count = spritecount.x * spritecount.y; frame < count; frame++) {
+ var name = "" + frame;
+ atlas[name] = {
+ name: name,
+ texture: "default",
+ // the source texture
+ offset: new me.Vector2d(margin + (spacing + data.framewidth) * (frame % spritecount.x), margin + (spacing + data.frameheight) * ~~(frame / spritecount.x)),
+ anchorPoint: data.anchorPoint || null,
+ trimmed: false,
+ width: data.framewidth,
+ height: data.frameheight,
+ angle: 0
+ };
+ this.addUvsMap(atlas, name, width, height);
+ }
+
+ me.pool.push(spritecount);
+ return atlas;
+ },
+
+ /**
+ * @ignore
+ */
+ addUvsMap: function addUvsMap(atlas, frame, w, h) {
+ // ignore if using the Canvas Renderer
+ if (me.video.renderer instanceof me.WebGLRenderer) {
+ // Source coordinates
+ var s = atlas[frame].offset;
+ var sw = atlas[frame].width;
+ var sh = atlas[frame].height;
+ atlas[frame].uvs = new Float32Array([s.x / w, // Left
+ s.y / h, // Top
+ (s.x + sw) / w, // Right
+ (s.y + sh) / h // Bottom
+ ]); // Cache source coordinates
+ // TODO: Remove this when the Batcher only accepts a region name
+
+ var key = s.x + "," + s.y + "," + w + "," + h;
+ atlas[key] = atlas[frame];
+ }
+
+ return atlas[frame];
+ },
+
+ /**
+ * @ignore
+ */
+ addQuadRegion: function addQuadRegion(name, x, y, w, h) {
+ // TODO: Require proper atlas regions instead of caching arbitrary region keys
+ if (me.video.renderer.settings.verbose === true) {
+ console.warn("Adding texture region", name, "for texture", this);
+ }
+
+ var source = this.getTexture();
+ var atlas = this.getAtlas();
+ var dw = source.width;
+ var dh = source.height;
+ atlas[name] = {
+ name: name,
+ offset: new me.Vector2d(x, y),
+ width: w,
+ height: h,
+ angle: 0
+ };
+ this.addUvsMap(atlas, name, dw, dh);
+ return atlas[name];
+ },
+
+ /**
+ * return the default or specified atlas dictionnary
+ * @name getAtlas
+ * @memberOf me.Renderer.Texture
+ * @function
+ * @param {String} [name] atlas name in case of multipack textures
+ * @return {Object}
+ */
+ getAtlas: function getAtlas(key) {
+ if (typeof key === "string") {
+ return this.atlases.get(key);
+ } else {
+ return this.atlases.values().next().value;
+ }
+ },
+
+ /**
+ * return the source texture for the given region (or default one if none specified)
+ * @name getTexture
+ * @memberOf me.Renderer.Texture
+ * @function
+ * @param {Object} [region] region name in case of multipack textures
+ * @return {HTMLImageElement|HTMLCanvasElement}
+ */
+ getTexture: function getTexture(region) {
+ if (_typeof(region) === "object" && typeof region.texture === "string") {
+ return this.sources.get(region.texture);
+ } else {
+ return this.sources.values().next().value;
+ }
+ },
+
+ /**
+ * return a normalized region (or frame) information for the specified sprite name
+ * @name getRegion
+ * @memberOf me.Renderer.Texture
+ * @function
+ * @param {String} name name of the sprite
+ * @param {String} [atlas] name of a specific atlas where to search for the region
+ * @return {Object}
+ */
+ getRegion: function getRegion(name, atlas) {
+ var region;
+
+ if (typeof atlas === "string") {
+ region = this.getAtlas(atlas)[name];
+ } else {
+ // look for the given region in each existing atlas
+ this.atlases.forEach(function (atlas) {
+ if (typeof atlas[name] !== "undefined") {
+ // there should be only one
+ region = atlas[name];
+ }
+ });
+ }
+
+ return region;
+ },
+
+ /**
+ * return the uvs mapping for the given region
+ * @name getUVs
+ * @memberOf me.Renderer.Texture
+ * @function
+ * @param {Object} region region (or frame) name
+ * @return {Float32Array} region Uvs
+ */
+ getUVs: function getUVs(name) {
+ // Get the source texture region
+ var region = this.getRegion(name);
+
+ if (typeof region === "undefined") {
+ // TODO: Require proper atlas regions instead of caching arbitrary region keys
+ var keys = name.split(","),
+ sx = +keys[0],
+ sy = +keys[1],
+ sw = +keys[2],
+ sh = +keys[3];
+ region = this.addQuadRegion(name, sx, sy, sw, sh);
+ }
+
+ return region.uvs;
+ },
+
+ /**
+ * Create a sprite object using the first region found using the specified name
+ * @name createSpriteFromName
+ * @memberOf me.Renderer.Texture
+ * @function
+ * @param {String} name name of the sprite
+ * @param {Object} [settings] Additional settings passed to the {@link me.Sprite} contructor
+ * @return {me.Sprite}
+ * @example
+ * // create a new texture object under the `game` namespace
+ * game.texture = new me.video.renderer.Texture(
+ * me.loader.getJSON("texture"),
+ * me.loader.getImage("texture")
+ * );
+ * ...
+ * ...
+ * // add the coin sprite as renderable for the entity
+ * this.renderable = game.texture.createSpriteFromName("coin.png");
+ * // set the renderable position to bottom center
+ * this.anchorPoint.set(0.5, 1.0);
+ */
+ createSpriteFromName: function createSpriteFromName(name, settings) {
+ // instantiate a new sprite object
+ return me.pool.pull("me.Sprite", 0, 0, Object.assign({
+ image: this,
+ region: name
+ }, settings || {}));
+ },
+
+ /**
+ * Create an animation object using the first region found using all specified names
+ * @name createAnimationFromName
+ * @memberOf me.Renderer.Texture
+ * @function
+ * @param {String[]|Number[]} names list of names for each sprite
+ * (when manually creating a Texture out of a spritesheet, only numeric values are authorized)
+ * @param {Object} [settings] Additional settings passed to the {@link me.Sprite} contructor
+ * @return {me.Sprite}
+ * @example
+ * // create a new texture object under the `game` namespace
+ * game.texture = new me.video.renderer.Texture(
+ * me.loader.getJSON("texture"),
+ * me.loader.getImage("texture")
+ * );
+ *
+ * // create a new Sprite as renderable for the entity
+ * this.renderable = game.texture.createAnimationFromName([
+ * "walk0001.png", "walk0002.png", "walk0003.png",
+ * "walk0004.png", "walk0005.png", "walk0006.png",
+ * "walk0007.png", "walk0008.png", "walk0009.png",
+ * "walk0010.png", "walk0011.png"
+ * ]);
+ *
+ * // define an additional basic walking animation
+ * this.renderable.addAnimation ("simple_walk", [0,2,1]);
+ * // you can also use frame name to define your animation
+ * this.renderable.addAnimation ("speed_walk", ["walk0007.png", "walk0008.png", "walk0009.png", "walk0010.png"]);
+ * // set the default animation
+ * this.renderable.setCurrentAnimation("simple_walk");
+ * // set the renderable position to bottom center
+ * this.anchorPoint.set(0.5, 1.0);
+ */
+ createAnimationFromName: function createAnimationFromName(names, settings) {
+ var tpAtlas = [],
+ indices = {};
+ var width = 0,
+ height = 0;
+ var region; // iterate through the given names
+ // and create a "normalized" atlas
+
+ for (var i = 0; i < names.length; ++i) {
+ region = this.getRegion(names[i]);
+
+ if (region == null) {
+ // throw an error
+ throw new Error("Texture - region for " + names[i] + " not found");
+ }
+
+ tpAtlas[i] = region; // save the corresponding index
+
+ indices[names[i]] = i; // calculate the max size of a frame
+
+ width = Math.max(region.width, width);
+ height = Math.max(region.height, height);
+ } // instantiate a new animation sheet object
+
+
+ return new me.Sprite(0, 0, Object.assign({
+ image: this,
+ framewidth: width,
+ frameheight: height,
+ margin: 0,
+ spacing: 0,
+ atlas: tpAtlas,
+ atlasIndices: indices
+ }, settings || {}));
+ }
+ });
+ })();
+
+ (function () {
+ /**
+ * a basic texture cache object
+ * @ignore
+ */
+ me.Renderer.TextureCache = me.Object.extend({
+ /**
+ * @ignore
+ */
+ init: function init(max_size) {
+ this.cache = new Map();
+ this.units = new Map();
+ this.max_size = max_size || Infinity;
+ this.clear();
+ },
+
+ /**
+ * @ignore
+ */
+ clear: function clear() {
+ this.cache.clear();
+ this.units.clear();
+ this.length = 0;
+ },
+
+ /**
+ * @ignore
+ */
+ validate: function validate() {
+ if (this.length >= this.max_size) {
+ // TODO: Merge textures instead of throwing an exception
+ throw new Error("Texture cache overflow: " + this.max_size + " texture units available.");
+ }
+ },
+
+ /**
+ * @ignore
+ */
+ get: function get(image, atlas) {
+ if (!this.cache.has(image)) {
+ if (!atlas) {
+ atlas = me.video.renderer.Texture.prototype.createAtlas.apply(me.video.renderer.Texture.prototype, [image.width, image.height, image.src ? me.utils.file.getBasename(image.src) : undefined]);
+ }
+
+ this.set(image, new me.video.renderer.Texture(atlas, image, false));
+ }
+
+ return this.cache.get(image);
+ },
+
+ /**
+ * @ignore
+ */
+ set: function set(image, texture) {
+ var width = image.width;
+ var height = image.height; // warn if a non POT texture is added to the cache
+
+ if (!me.Math.isPowerOfTwo(width) || !me.Math.isPowerOfTwo(height)) {
+ var src = typeof image.src !== "undefined" ? image.src : image;
+ console.warn("[Texture] " + src + " is not a POT texture " + "(" + width + "x" + height + ")");
+ }
+
+ this.validate();
+ this.cache.set(image, texture);
+ this.units.set(texture, this.length++);
+ },
+
+ /**
+ * @ignore
+ */
+ getUnit: function getUnit(texture) {
+ return this.units.get(texture);
+ }
+ });
+ })();
+
+ (function () {
+ /**
+ * a canvas renderer object
+ * @class
+ * @extends me.Renderer
+ * @memberOf me
+ * @constructor
+ * @param {HTMLCanvasElement} canvas The html canvas tag to draw to on screen.
+ * @param {Number} width The width of the canvas without scaling
+ * @param {Number} height The height of the canvas without scaling
+ * @param {Object} [options] The renderer parameters
+ * @param {Boolean} [options.doubleBuffering=false] Whether to enable double buffering
+ * @param {Boolean} [options.antiAlias=false] Whether to enable anti-aliasing
+ * @param {Boolean} [options.transparent=false] Whether to enable transparency on the canvas (performance hit when enabled)
+ * @param {Boolean} [options.subPixel=false] Whether to enable subpixel renderering (performance hit when enabled)
+ * @param {Boolean} [options.textureSeamFix=true] enable the texture seam fix when rendering Tile when antiAlias is off for the canvasRenderer
+ * @param {Number} [options.zoomX=width] The actual width of the canvas with scaling applied
+ * @param {Number} [options.zoomY=height] The actual height of the canvas with scaling applied
+ */
+ me.CanvasRenderer = me.Renderer.extend({
+ /**
+ * @ignore
+ */
+ init: function init(c, width, height, options) {
+ // parent constructor
+ this._super(me.Renderer, "init", [c, width, height, options]); // defined the 2d context
+
+
+ this.context = this.getContext2d(this.canvas, this.settings.transparent); // create the back buffer if we use double buffering
+
+ if (this.settings.doubleBuffering) {
+ this.backBufferCanvas = me.video.createCanvas(width, height);
+ this.backBufferContext2D = this.getContext2d(this.backBufferCanvas);
+
+ if (this.settings.transparent) {
+ // Clears the front buffer for each frame blit
+ this.context.globalCompositeOperation = "copy";
+ }
+ } else {
+ this.backBufferCanvas = this.canvas;
+ this.backBufferContext2D = this.context;
+ }
+
+ this.setBlendMode(this.settings.blendMode); // apply the default color to the 2d context
+
+ this.setColor(this.currentColor); // create a texture cache
+
+ this.cache = new me.Renderer.TextureCache();
+
+ if (this.settings.textureSeamFix !== false && !this.settings.antiAlias) {
+ // enable the tile texture seam fix with the canvas renderer
+ this.uvOffset = 1;
+ }
+
+ return this;
+ },
+
+ /**
+ * Reset context state
+ * @name reset
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ */
+ reset: function reset() {
+ this._super(me.Renderer, "reset");
+ },
+
+ /**
+ * Reset the canvas transform to identity
+ * @name resetTransform
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ */
+ resetTransform: function resetTransform() {
+ this.backBufferContext2D.setTransform(1, 0, 0, 1, 0, 0);
+ },
+
+ /**
+ * Set a blend mode for the given context
+ * @name setBlendMode
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {String} [mode="normal"] blend mode : "normal", "multiply"
+ * @param {Context2d} [context]
+ */
+ setBlendMode: function setBlendMode(mode, context) {
+ context = context || this.getContext();
+ this.currentBlendMode = mode;
+
+ switch (mode) {
+ case "multiply":
+ context.globalCompositeOperation = "multiply";
+ break;
+
+ default:
+ // normal
+ context.globalCompositeOperation = "source-over";
+ this.currentBlendMode = "normal";
+ break;
+ }
+ },
+
+ /**
+ * prepare the framebuffer for drawing a new frame
+ * @name clear
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ */
+ clear: function clear() {
+ if (this.settings.transparent) {
+ this.clearColor("rgba(0,0,0,0)", true);
+ }
+ },
+
+ /**
+ * render the main framebuffer on screen
+ * @name flush
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ */
+ flush: function flush() {
+ if (this.settings.doubleBuffering) {
+ this.context.drawImage(this.backBufferCanvas, 0, 0, this.backBufferCanvas.width, this.backBufferCanvas.height, 0, 0, this.gameWidthZoom, this.gameHeightZoom);
+ }
+ },
+
+ /**
+ * Clears the main framebuffer with the given color
+ * @name clearColor
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {me.Color|String} color CSS color.
+ * @param {Boolean} [opaque=false] Allow transparency [default] or clear the surface completely [true]
+ */
+ clearColor: function clearColor(col, opaque) {
+ this.save();
+ this.resetTransform();
+ this.backBufferContext2D.globalCompositeOperation = opaque ? "copy" : "source-over";
+ this.backBufferContext2D.fillStyle = col instanceof me.Color ? col.toRGBA() : col;
+ this.fillRect(0, 0, this.backBufferCanvas.width, this.backBufferCanvas.height);
+ this.restore();
+ },
+
+ /**
+ * Erase the pixels in the given rectangular area by setting them to transparent black (rgba(0,0,0,0)).
+ * @name clearRect
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {Number} x x axis of the coordinate for the rectangle starting point.
+ * @param {Number} y y axis of the coordinate for the rectangle starting point.
+ * @param {Number} width The rectangle's width.
+ * @param {Number} height The rectangle's height.
+ */
+ clearRect: function clearRect(x, y, width, height) {
+ this.backBufferContext2D.clearRect(x, y, width, height);
+ },
+
+ /**
+ * Create a pattern with the specified repetition
+ * @name createPattern
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {image} image Source image
+ * @param {String} repeat Define how the pattern should be repeated
+ * @return {CanvasPattern}
+ * @see me.ImageLayer#repeat
+ * @example
+ * var tileable = renderer.createPattern(image, "repeat");
+ * var horizontal = renderer.createPattern(image, "repeat-x");
+ * var vertical = renderer.createPattern(image, "repeat-y");
+ * var basic = renderer.createPattern(image, "no-repeat");
+ */
+ createPattern: function createPattern(image, repeat) {
+ return this.backBufferContext2D.createPattern(image, repeat);
+ },
+
+ /**
+ * Draw an image onto the main using the canvas api
+ * @name drawImage
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {Image} image An element to draw into the context. The specification permits any canvas image source (CanvasImageSource), specifically, a CSSImageValue, an HTMLImageElement, an SVGImageElement, an HTMLVideoElement, an HTMLCanvasElement, an ImageBitmap, or an OffscreenCanvas.
+ * @param {Number} sx The X coordinate of the top left corner of the sub-rectangle of the source image to draw into the destination context.
+ * @param {Number} sy The Y coordinate of the top left corner of the sub-rectangle of the source image to draw into the destination context.
+ * @param {Number} sw The width of the sub-rectangle of the source image to draw into the destination context. If not specified, the entire rectangle from the coordinates specified by sx and sy to the bottom-right corner of the image is used.
+ * @param {Number} sh The height of the sub-rectangle of the source image to draw into the destination context.
+ * @param {Number} dx The X coordinate in the destination canvas at which to place the top-left corner of the source image.
+ * @param {Number} dy The Y coordinate in the destination canvas at which to place the top-left corner of the source image.
+ * @param {Number} dWidth The width to draw the image in the destination canvas. This allows scaling of the drawn image. If not specified, the image is not scaled in width when drawn.
+ * @param {Number} dHeight The height to draw the image in the destination canvas. This allows scaling of the drawn image. If not specified, the image is not scaled in height when drawn.
+ * @example
+ * // Position the image on the canvas:
+ * renderer.drawImage(image, dx, dy);
+ * // Position the image on the canvas, and specify width and height of the image:
+ * renderer.drawImage(image, dx, dy, dWidth, dHeight);
+ * // Clip the image and position the clipped part on the canvas:
+ * renderer.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
+ */
+ drawImage: function drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh) {
+ if (this.backBufferContext2D.globalAlpha < 1 / 255) {
+ // Fast path: don't draw fully transparent
+ return;
+ }
+
+ if (typeof sw === "undefined") {
+ sw = dw = image.width;
+ sh = dh = image.height;
+ dx = sx;
+ dy = sy;
+ sx = 0;
+ sy = 0;
+ } else if (typeof dx === "undefined") {
+ dx = sx;
+ dy = sy;
+ dw = sw;
+ dh = sh;
+ sw = image.width;
+ sh = image.height;
+ sx = 0;
+ sy = 0;
+ }
+
+ if (this.settings.subPixel === false) {
+ // clamp to pixel grid
+ dx = ~~dx;
+ dy = ~~dy;
+ }
+
+ this.backBufferContext2D.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);
+ },
+
+ /**
+ * Draw a pattern within the given rectangle.
+ * @name drawPattern
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {CanvasPattern} pattern Pattern object
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} width
+ * @param {Number} height
+ * @see me.CanvasRenderer#createPattern
+ */
+ drawPattern: function drawPattern(pattern, x, y, width, height) {
+ if (this.backBufferContext2D.globalAlpha < 1 / 255) {
+ // Fast path: don't draw fully transparent
+ return;
+ }
+
+ var fillStyle = this.backBufferContext2D.fillStyle;
+ this.backBufferContext2D.fillStyle = pattern;
+ this.backBufferContext2D.fillRect(x, y, width, height);
+ this.backBufferContext2D.fillStyle = fillStyle;
+ },
+
+ /**
+ * Stroke an arc at the specified coordinates with given radius, start and end points
+ * @name strokeArc
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {Number} x arc center point x-axis
+ * @param {Number} y arc center point y-axis
+ * @param {Number} radius
+ * @param {Number} start start angle in radians
+ * @param {Number} end end angle in radians
+ * @param {Boolean} [antiClockwise=false] draw arc anti-clockwise
+ */
+ strokeArc: function strokeArc(x, y, radius, start, end, antiClockwise, fill) {
+ var context = this.backBufferContext2D;
+
+ if (context.globalAlpha < 1 / 255) {
+ // Fast path: don't draw fully transparent
+ return;
+ }
+
+ this.translate(x + radius, y + radius);
+ context.beginPath();
+ context.arc(0, 0, radius, start, end, antiClockwise || false);
+ context[fill === true ? "fill" : "stroke"]();
+ context.closePath();
+ this.translate(-(x + radius), -(y + radius));
+ },
+
+ /**
+ * Fill an arc at the specified coordinates with given radius, start and end points
+ * @name fillArc
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {Number} x arc center point x-axis
+ * @param {Number} y arc center point y-axis
+ * @param {Number} radius
+ * @param {Number} start start angle in radians
+ * @param {Number} end end angle in radians
+ * @param {Boolean} [antiClockwise=false] draw arc anti-clockwise
+ */
+ fillArc: function fillArc(x, y, radius, start, end, antiClockwise) {
+ this.strokeArc(x, y, radius, start, end, antiClockwise || false, true);
+ },
+
+ /**
+ * Stroke an ellipse at the specified coordinates with given radius
+ * @name strokeEllipse
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {Number} x ellipse center point x-axis
+ * @param {Number} y ellipse center point y-axis
+ * @param {Number} w horizontal radius of the ellipse
+ * @param {Number} h vertical radius of the ellipse
+ */
+ strokeEllipse: function strokeEllipse(x, y, w, h, fill) {
+ var context = this.backBufferContext2D;
+
+ if (context.globalAlpha < 1 / 255) {
+ // Fast path: don't draw fully transparent
+ return;
+ }
+
+ var hw = w,
+ hh = h,
+ lx = x - hw,
+ rx = x + hw,
+ ty = y - hh,
+ by = y + hh;
+ var xmagic = hw * 0.551784,
+ ymagic = hh * 0.551784,
+ xmin = x - xmagic,
+ xmax = x + xmagic,
+ ymin = y - ymagic,
+ ymax = y + ymagic;
+ context.beginPath();
+ context.moveTo(x, ty);
+ context.bezierCurveTo(xmax, ty, rx, ymin, rx, y);
+ context.bezierCurveTo(rx, ymax, xmax, by, x, by);
+ context.bezierCurveTo(xmin, by, lx, ymax, lx, y);
+ context.bezierCurveTo(lx, ymin, xmin, ty, x, ty);
+ context[fill === true ? "fill" : "stroke"]();
+ },
+
+ /**
+ * Fill an ellipse at the specified coordinates with given radius
+ * @name fillEllipse
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {Number} x ellipse center point x-axis
+ * @param {Number} y ellipse center point y-axis
+ * @param {Number} w horizontal radius of the ellipse
+ * @param {Number} h vertical radius of the ellipse
+ */
+ fillEllipse: function fillEllipse(x, y, w, h) {
+ this.strokeEllipse(x, y, w, h, true);
+ },
+
+ /**
+ * Stroke a line of the given two points
+ * @name strokeLine
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {Number} startX the start x coordinate
+ * @param {Number} startY the start y coordinate
+ * @param {Number} endX the end x coordinate
+ * @param {Number} endY the end y coordinate
+ */
+ strokeLine: function strokeLine(startX, startY, endX, endY) {
+ var context = this.backBufferContext2D;
+
+ if (context < 1 / 255) {
+ // Fast path: don't draw fully transparent
+ return;
+ }
+
+ context.beginPath();
+ context.moveTo(startX, startY);
+ context.lineTo(endX, endY);
+ context.stroke();
+ },
+
+ /**
+ * Fill a line of the given two points
+ * @name fillLine
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {Number} startX the start x coordinate
+ * @param {Number} startY the start y coordinate
+ * @param {Number} endX the end x coordinate
+ * @param {Number} endY the end y coordinate
+ */
+ fillLine: function fillLine(startX, startY, endX, endY) {
+ this.strokeLine(startX, startY, endX, endY);
+ },
+
+ /**
+ * Stroke the given me.Polygon on the screen
+ * @name strokePolygon
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {me.Polygon} poly the shape to draw
+ */
+ strokePolygon: function strokePolygon(poly, fill) {
+ var context = this.backBufferContext2D;
+
+ if (context.globalAlpha < 1 / 255) {
+ // Fast path: don't draw fully transparent
+ return;
+ }
+
+ this.translate(poly.pos.x, poly.pos.y);
+ context.beginPath();
+ context.moveTo(poly.points[0].x, poly.points[0].y);
+ var point;
+
+ for (var i = 1; i < poly.points.length; i++) {
+ point = poly.points[i];
+ context.lineTo(point.x, point.y);
+ }
+
+ context.lineTo(poly.points[0].x, poly.points[0].y);
+ context[fill === true ? "fill" : "stroke"]();
+ context.closePath();
+ this.translate(-poly.pos.x, -poly.pos.y);
+ },
+
+ /**
+ * Fill the given me.Polygon on the screen
+ * @name fillPolygon
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {me.Polygon} poly the shape to draw
+ */
+ fillPolygon: function fillPolygon(poly) {
+ this.strokePolygon(poly, true);
+ },
+
+ /**
+ * Stroke a rectangle at the specified coordinates
+ * @name strokeRect
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} width
+ * @param {Number} height
+ */
+ strokeRect: function strokeRect(x, y, width, height) {
+ if (this.backBufferContext2D.globalAlpha < 1 / 255) {
+ // Fast path: don't draw fully transparent
+ return;
+ }
+
+ this.backBufferContext2D.strokeRect(x, y, width, height);
+ },
+
+ /**
+ * Draw a filled rectangle at the specified coordinates
+ * @name fillRect
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} width
+ * @param {Number} height
+ */
+ fillRect: function fillRect(x, y, width, height) {
+ if (this.backBufferContext2D.globalAlpha < 1 / 255) {
+ // Fast path: don't draw fully transparent
+ return;
+ }
+
+ this.backBufferContext2D.fillRect(x, y, width, height);
+ },
+
+ /**
+ * return a reference to the system 2d Context
+ * @name getContext
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @return {CanvasRenderingContext2D}
+ */
+ getContext: function getContext() {
+ return this.backBufferContext2D;
+ },
+
+ /**
+ * return a reference to the font 2d Context
+ * @ignore
+ */
+ getFontContext: function getFontContext() {
+ // in canvas mode we can directly use the 2d context
+ return this.getContext();
+ },
+
+ /**
+ * scales the canvas & 2d Context
+ * @name scaleCanvas
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ */
+ scaleCanvas: function scaleCanvas(scaleX, scaleY) {
+ this.canvas.width = this.gameWidthZoom = this.backBufferCanvas.width * scaleX;
+ this.canvas.height = this.gameHeightZoom = this.backBufferCanvas.height * scaleY; // adjust CSS style for High-DPI devices
+
+ if (me.device.devicePixelRatio > 1) {
+ this.canvas.style.width = this.canvas.width / me.device.devicePixelRatio + "px";
+ this.canvas.style.height = this.canvas.height / me.device.devicePixelRatio + "px";
+ }
+
+ if (this.settings.doubleBuffering && this.settings.transparent) {
+ // Clears the front buffer for each frame blit
+ this.context.globalCompositeOperation = "copy";
+ } else {
+ this.setBlendMode(this.settings.blendMode, this.context);
+ }
+
+ this.setAntiAlias(this.context, this.settings.antiAlias);
+ this.flush();
+ },
+
+ /**
+ * save the canvas context
+ * @name save
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ */
+ save: function save() {
+ this.backBufferContext2D.save();
+ },
+
+ /**
+ * restores the canvas context
+ * @name restore
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ */
+ restore: function restore() {
+ this.backBufferContext2D.restore();
+ this.currentColor.glArray[3] = this.backBufferContext2D.globalAlpha;
+ this.currentScissor[0] = 0;
+ this.currentScissor[1] = 0;
+ this.currentScissor[2] = this.backBufferCanvas.width;
+ this.currentScissor[3] = this.backBufferCanvas.height;
+ },
+
+ /**
+ * rotates the canvas context
+ * @name rotate
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {Number} angle in radians
+ */
+ rotate: function rotate(angle) {
+ this.backBufferContext2D.rotate(angle);
+ },
+
+ /**
+ * scales the canvas context
+ * @name scale
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {Number} x
+ * @param {Number} y
+ */
+ scale: function scale(x, y) {
+ this.backBufferContext2D.scale(x, y);
+ },
+
+ /**
+ * Set the current fill & stroke style color.
+ * By default, or upon reset, the value is set to #000000.
+ * @name setColor
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {me.Color|String} color css color value
+ */
+ setColor: function setColor(color) {
+ this.backBufferContext2D.strokeStyle = this.backBufferContext2D.fillStyle = color instanceof me.Color ? color.toRGBA() : color;
+ },
+
+ /**
+ * Set the global alpha on the canvas context
+ * @name setGlobalAlpha
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {Number} alpha 0.0 to 1.0 values accepted.
+ */
+ setGlobalAlpha: function setGlobalAlpha(a) {
+ this.backBufferContext2D.globalAlpha = this.currentColor.glArray[3] = a;
+ },
+
+ /**
+ * Set the line width on the context
+ * @name setLineWidth
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {Number} width Line width
+ */
+ setLineWidth: function setLineWidth(width) {
+ this.backBufferContext2D.lineWidth = width;
+ },
+
+ /**
+ * Reset (overrides) the renderer transformation matrix to the
+ * identity one, and then apply the given transformation matrix.
+ * @name setTransform
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {me.Matrix2d} mat2d Matrix to transform by
+ */
+ setTransform: function setTransform(mat2d) {
+ this.resetTransform();
+ this.transform(mat2d);
+ },
+
+ /**
+ * Multiply given matrix into the renderer tranformation matrix
+ * @name transform
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {me.Matrix2d} mat2d Matrix to transform by
+ */
+ transform: function transform(mat2d) {
+ var a = mat2d.val;
+ var tx = a[6],
+ ty = a[7];
+
+ if (this.settings.subPixel === false) {
+ tx = ~~tx;
+ ty = ~~ty;
+ }
+
+ this.backBufferContext2D.transform(a[0], a[1], a[3], a[4], tx, ty);
+ },
+
+ /**
+ * Translates the context to the given position
+ * @name translate
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {Number} x
+ * @param {Number} y
+ */
+ translate: function translate(x, y) {
+ if (this.settings.subPixel === false) {
+ this.backBufferContext2D.translate(~~x, ~~y);
+ } else {
+ this.backBufferContext2D.translate(x, y);
+ }
+ },
+
+ /**
+ * clip the given region from the original canvas. Once a region is clipped,
+ * all future drawing will be limited to the clipped region.
+ * You can however save the current region using the save(),
+ * and restore it (with the restore() method) any time in the future.
+ * (this is an experimental feature !)
+ * @name clipRect
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} width
+ * @param {Number} height
+ */
+ clipRect: function clipRect(x, y, width, height) {
+ var canvas = this.backBufferCanvas; // if requested box is different from the current canvas size;
+
+ if (x !== 0 || y !== 0 || width !== canvas.width || height !== canvas.height) {
+ var currentScissor = this.currentScissor; // if different from the current scissor box
+
+ if (currentScissor[0] !== x || currentScissor[1] !== y || currentScissor[2] !== width || currentScissor[3] !== height) {
+ var context = this.backBufferContext2D;
+ context.beginPath();
+ context.rect(x, y, width, height);
+ context.clip(); // save the new currentScissor box
+
+ currentScissor[0] = x;
+ currentScissor[1] = y;
+ currentScissor[2] = width;
+ currentScissor[3] = height;
+ }
+ }
+ },
+
+ /**
+ * A mask limits rendering elements to the shape and position of the given mask object.
+ * So, if the renderable is larger than the mask, only the intersecting part of the renderable will be visible.
+ * Mask are not preserved through renderer context save and restore.
+ * @name setMask
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ * @param {me.Rect|me.Polygon|me.Line|me.Ellipse} [mask] the shape defining the mask to be applied
+ */
+ setMask: function setMask(mask) {
+ var context = this.backBufferContext2D;
+ var _x = mask.pos.x,
+ _y = mask.pos.y; // https://github.com/melonjs/melonJS/issues/648
+
+ if (mask instanceof me.Ellipse) {
+ var hw = mask.radiusV.x,
+ hh = mask.radiusV.y,
+ lx = _x - hw,
+ rx = _x + hw,
+ ty = _y - hh,
+ by = _y + hh;
+ var xmagic = hw * 0.551784,
+ ymagic = hh * 0.551784,
+ xmin = _x - xmagic,
+ xmax = _x + xmagic,
+ ymin = _y - ymagic,
+ ymax = _y + ymagic;
+ context.beginPath();
+ context.moveTo(_x, ty);
+ context.bezierCurveTo(xmax, ty, rx, ymin, rx, _y);
+ context.bezierCurveTo(rx, ymax, xmax, by, _x, by);
+ context.bezierCurveTo(xmin, by, lx, ymax, lx, _y);
+ context.bezierCurveTo(lx, ymin, xmin, ty, _x, ty);
+ } else {
+ context.save();
+ context.beginPath();
+ context.moveTo(_x + mask.points[0].x, _y + mask.points[0].y);
+ var point;
+
+ for (var i = 1; i < mask.points.length; i++) {
+ point = mask.points[i];
+ context.lineTo(_x + point.x, _y + point.y);
+ }
+
+ context.closePath();
+ }
+
+ context.clip();
+ },
+
+ /**
+ * disable (remove) the rendering mask set through setMask.
+ * @name clearMask
+ * @see setMask
+ * @memberOf me.CanvasRenderer.prototype
+ * @function
+ */
+ clearMask: function clearMask() {
+ this.backBufferContext2D.restore();
+ }
+ });
+ })();
+
+ (function () {
+ /**
+ * a WebGL renderer object
+ * @extends me.Renderer
+ * @namespace me.WebGLRenderer
+ * @memberOf me
+ * @constructor
+ * @param {HTMLCanvasElement} canvas The html canvas tag to draw to on screen.
+ * @param {Number} width The width of the canvas without scaling
+ * @param {Number} height The height of the canvas without scaling
+ * @param {Object} [options] The renderer parameters
+ * @param {Boolean} [options.doubleBuffering=false] Whether to enable double buffering
+ * @param {Boolean} [options.antiAlias=false] Whether to enable anti-aliasing
+ * @param {Boolean} [options.failIfMajorPerformanceCaveat=true] If true, the renderer will switch to CANVAS mode if the performances of a WebGL context would be dramatically lower than that of a native application making equivalent OpenGL calls.
+ * @param {Boolean} [options.transparent=false] Whether to enable transparency on the canvas (performance hit when enabled)
+ * @param {Boolean} [options.subPixel=false] Whether to enable subpixel renderering (performance hit when enabled)
+ * @param {Number} [options.zoomX=width] The actual width of the canvas with scaling applied
+ * @param {Number} [options.zoomY=height] The actual height of the canvas with scaling applied
+ * @param {me.WebGLRenderer.Compositor} [options.compositor] A class that implements the compositor API
+ */
+ me.WebGLRenderer = me.Renderer.extend({
+ /**
+ * @ignore
+ */
+ init: function init(canvas, width, height, options) {
+ // reference to this renderer
+ var renderer = this; // parent contructor
+
+ this._super(me.Renderer, "init", [canvas, width, height, options]);
+ /**
+ * The WebGL context
+ * @name gl
+ * @memberOf me.WebGLRenderer
+ * type {WebGLRenderingContext}
+ */
+
+
+ this.context = this.gl = this.getContextGL(canvas, this.settings.transparent);
+ /**
+ * @ignore
+ */
+
+ this._colorStack = [];
+ /**
+ * @ignore
+ */
+
+ this._matrixStack = [];
+ /**
+ * @ignore
+ */
+
+ this._scissorStack = [];
+ /**
+ * @ignore
+ */
+
+ this._glPoints = [new me.Vector2d(), new me.Vector2d(), new me.Vector2d(), new me.Vector2d()];
+ /**
+ * The current transformation matrix used for transformations on the overall scene
+ * @name currentTransform
+ * @type me.Matrix2d
+ * @memberOf me.WebGLRenderer#
+ */
+
+ this.currentTransform = new me.Matrix2d(); // Create a compositor
+
+ var Compositor = this.settings.compositor || me.WebGLRenderer.Compositor;
+ this.compositor = new Compositor(this); // default WebGL state(s)
+
+ this.gl.disable(this.gl.DEPTH_TEST);
+ this.gl.disable(this.gl.SCISSOR_TEST);
+ this.gl.enable(this.gl.BLEND); // set default mode
+
+ this.setBlendMode(this.settings.blendMode); // Create a texture cache
+
+ this.cache = new me.Renderer.TextureCache(this.compositor.maxTextures); // Configure the WebGL viewport
+
+ this.scaleCanvas(1, 1); // to simulate context lost and restore :
+ // var ctx = me.video.renderer.context.getExtension('WEBGL_lose_context');
+ // ctx.loseContext()
+
+ canvas.addEventListener("webglcontextlost", function (event) {
+ event.preventDefault();
+ renderer.isContextValid = false;
+ me.event.publish(me.event.WEBGL_ONCONTEXT_LOST, [renderer]);
+ }, false); // ctx.restoreContext()
+
+ canvas.addEventListener("webglcontextrestored", function (event) {
+ renderer.reset();
+ renderer.isContextValid = true;
+ me.event.publish(me.event.WEBGL_ONCONTEXT_RESTORED, [renderer]);
+ }, false);
+ return this;
+ },
+
+ /**
+ * Reset context state
+ * @name reset
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ */
+ reset: function reset() {
+ this._super(me.Renderer, "reset");
+
+ if (this.isContextValid === false) {
+ // on context lost/restore
+ this.compositor.init(this);
+ } else {
+ this.compositor.reset();
+ }
+
+ this.gl.disable(this.gl.SCISSOR_TEST);
+
+ if (typeof this.fontContext2D !== "undefined") {
+ this.createFontTexture(this.cache);
+ }
+ },
+
+ /**
+ * Reset the gl transform to identity
+ * @name resetTransform
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ */
+ resetTransform: function resetTransform() {
+ this.currentTransform.identity();
+ },
+
+ /**
+ * @ignore
+ */
+ createFontTexture: function createFontTexture(cache) {
+ if (typeof this.fontTexture === "undefined") {
+ var image = me.video.createCanvas(me.Math.nextPowerOfTwo(this.backBufferCanvas.width), me.Math.nextPowerOfTwo(this.backBufferCanvas.height));
+ /**
+ * @ignore
+ */
+
+ this.fontContext2D = this.getContext2d(image);
+ /**
+ * @ignore
+ */
+
+ this.fontTexture = new this.Texture(this.Texture.prototype.createAtlas.apply(this.Texture.prototype, [this.backBufferCanvas.width, this.backBufferCanvas.height, "fontTexture"]), image, cache);
+ } else {
+ // fontTexture was already created, just add it back into the cache
+ cache.set(this.fontContext2D.canvas, this.fontTexture);
+ }
+
+ this.compositor.uploadTexture(this.fontTexture, 0, 0, 0);
+ },
+
+ /**
+ * Create a pattern with the specified repetition
+ * @name createPattern
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {image} image Source image
+ * @param {String} repeat Define how the pattern should be repeated
+ * @return {me.video.renderer.Texture}
+ * @see me.ImageLayer#repeat
+ * @example
+ * var tileable = renderer.createPattern(image, "repeat");
+ * var horizontal = renderer.createPattern(image, "repeat-x");
+ * var vertical = renderer.createPattern(image, "repeat-y");
+ * var basic = renderer.createPattern(image, "no-repeat");
+ */
+ createPattern: function createPattern(image, repeat) {
+ if (!me.Math.isPowerOfTwo(image.width) || !me.Math.isPowerOfTwo(image.height)) {
+ var src = typeof image.src !== "undefined" ? image.src : image;
+ throw new Error("[WebGL Renderer] " + src + " is not a POT texture " + "(" + image.width + "x" + image.height + ")");
+ }
+
+ var texture = new this.Texture(this.Texture.prototype.createAtlas.apply(this.Texture.prototype, [image.width, image.height, "pattern", repeat]), image); // FIXME: Remove old cache entry and texture when changing the repeat mode
+
+ this.compositor.uploadTexture(texture);
+ return texture;
+ },
+
+ /**
+ * Flush the compositor to the frame buffer
+ * @name flush
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ */
+ flush: function flush() {
+ this.compositor.flush();
+ },
+
+ /**
+ * Clears the gl context with the given color.
+ * @name clearColor
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {me.Color|String} color CSS color.
+ * @param {Boolean} [opaque=false] Allow transparency [default] or clear the surface completely [true]
+ */
+ clearColor: function clearColor(col, opaque) {
+ this.save();
+ this.resetTransform();
+ this.currentColor.copy(col);
+
+ if (opaque) {
+ this.compositor.clear();
+ } else {
+ this.fillRect(0, 0, this.canvas.width, this.canvas.height);
+ }
+
+ this.restore();
+ },
+
+ /**
+ * Erase the pixels in the given rectangular area by setting them to transparent black (rgba(0,0,0,0)).
+ * @name clearRect
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {Number} x x axis of the coordinate for the rectangle starting point.
+ * @param {Number} y y axis of the coordinate for the rectangle starting point.
+ * @param {Number} width The rectangle's width.
+ * @param {Number} height The rectangle's height.
+ */
+ clearRect: function clearRect(x, y, width, height) {
+ var color = this.currentColor.clone();
+ this.currentColor.copy("#000000");
+ this.fillRect(x, y, width, height);
+ this.currentColor.copy(color);
+ me.pool.push(color);
+ },
+
+ /**
+ * @ignore
+ */
+ drawFont: function drawFont(bounds) {
+ var fontContext = this.getFontContext(); // Flush the compositor so we can upload a new texture
+
+ this.flush(); // Force-upload the new texture
+
+ this.compositor.uploadTexture(this.fontTexture, 0, 0, 0, true); // Add the new quad
+
+ var key = bounds.pos.x + "," + bounds.pos.y + "," + bounds.width + "," + bounds.height;
+ this.compositor.addQuad(this.fontTexture, key, bounds.pos.x, bounds.pos.y, bounds.width, bounds.height); // Clear font context2D
+
+ fontContext.clearRect(bounds.pos.x, bounds.pos.y, bounds.width, bounds.height);
+ },
+
+ /**
+ * Draw an image to the gl context
+ * @name drawImage
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {Image} image An element to draw into the context. The specification permits any canvas image source (CanvasImageSource), specifically, a CSSImageValue, an HTMLImageElement, an SVGImageElement, an HTMLVideoElement, an HTMLCanvasElement, an ImageBitmap, or an OffscreenCanvas.
+ * @param {Number} sx The X coordinate of the top left corner of the sub-rectangle of the source image to draw into the destination context.
+ * @param {Number} sy The Y coordinate of the top left corner of the sub-rectangle of the source image to draw into the destination context.
+ * @param {Number} sw The width of the sub-rectangle of the source image to draw into the destination context. If not specified, the entire rectangle from the coordinates specified by sx and sy to the bottom-right corner of the image is used.
+ * @param {Number} sh The height of the sub-rectangle of the source image to draw into the destination context.
+ * @param {Number} dx The X coordinate in the destination canvas at which to place the top-left corner of the source image.
+ * @param {Number} dy The Y coordinate in the destination canvas at which to place the top-left corner of the source image.
+ * @param {Number} dWidth The width to draw the image in the destination canvas. This allows scaling of the drawn image. If not specified, the image is not scaled in width when drawn.
+ * @param {Number} dHeight The height to draw the image in the destination canvas. This allows scaling of the drawn image. If not specified, the image is not scaled in height when drawn.
+ * @example
+ * // Position the image on the canvas:
+ * renderer.drawImage(image, dx, dy);
+ * // Position the image on the canvas, and specify width and height of the image:
+ * renderer.drawImage(image, dx, dy, dWidth, dHeight);
+ * // Clip the image and position the clipped part on the canvas:
+ * renderer.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
+ */
+ drawImage: function drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh) {
+ if (typeof sw === "undefined") {
+ sw = dw = image.width;
+ sh = dh = image.height;
+ dx = sx;
+ dy = sy;
+ sx = 0;
+ sy = 0;
+ } else if (typeof dx === "undefined") {
+ dx = sx;
+ dy = sy;
+ dw = sw;
+ dh = sh;
+ sw = image.width;
+ sh = image.height;
+ sx = 0;
+ sy = 0;
+ }
+
+ if (this.settings.subPixel === false) {
+ // clamp to pixel grid
+ dx = ~~dx;
+ dy = ~~dy;
+ }
+
+ var key = sx + "," + sy + "," + sw + "," + sh;
+ this.compositor.addQuad(this.cache.get(image), key, dx, dy, dw, dh);
+ },
+
+ /**
+ * Draw a pattern within the given rectangle.
+ * @name drawPattern
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {me.video.renderer.Texture} pattern Pattern object
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} width
+ * @param {Number} height
+ * @see me.WebGLRenderer#createPattern
+ */
+ drawPattern: function drawPattern(pattern, x, y, width, height) {
+ var key = "0,0," + width + "," + height;
+ this.compositor.addQuad(pattern, key, x, y, width, height);
+ },
+
+ /**
+ * return a reference to the screen canvas corresponding WebGL Context
+ * @name getScreenContext
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @return {WebGLRenderingContext}
+ */
+ getScreenContext: function getScreenContext() {
+ return this.gl;
+ },
+
+ /**
+ * Returns the WebGL Context object of the given Canvas
+ * @name getContextGL
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {Canvas} canvas
+ * @param {Boolean} [transparent=true] use false to disable transparency
+ * @return {WebGLRenderingContext}
+ */
+ getContextGL: function getContextGL(canvas, transparent) {
+ if (typeof canvas === "undefined" || canvas === null) {
+ throw new Error("You must pass a canvas element in order to create " + "a GL context");
+ }
+
+ if (typeof transparent !== "boolean") {
+ transparent = true;
+ }
+
+ var attr = {
+ alpha: transparent,
+ antialias: this.settings.antiAlias,
+ depth: false,
+ stencil: true,
+ premultipliedAlpha: transparent,
+ failIfMajorPerformanceCaveat: this.settings.failIfMajorPerformanceCaveat
+ };
+ var gl = canvas.getContext("webgl", attr) || canvas.getContext("experimental-webgl", attr);
+
+ if (!gl) {
+ throw new Error("A WebGL context could not be created.");
+ }
+
+ return gl;
+ },
+
+ /**
+ * Returns the WebGLContext instance for the renderer
+ * return a reference to the system 2d Context
+ * @name getContext
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @return {WebGLRenderingContext}
+ */
+ getContext: function getContext() {
+ return this.gl;
+ },
+
+ /**
+ * set a blend mode for the given context
+ * @name setBlendMode
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {String} [mode="normal"] blend mode : "normal", "multiply"
+ * @param {WebGLRenderingContext} [gl]
+ */
+ setBlendMode: function setBlendMode(mode, gl) {
+ gl = gl || this.gl;
+ gl.enable(gl.BLEND);
+
+ switch (mode) {
+ case "multiply":
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
+ this.currentBlendMode = mode;
+ break;
+
+ default:
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+ this.currentBlendMode = "normal";
+ break;
+ }
+ },
+
+ /**
+ * return a reference to the font 2d Context
+ * @ignore
+ */
+ getFontContext: function getFontContext() {
+ if (typeof this.fontContext2D === "undefined") {
+ // warn the end user about performance impact
+ console.warn("[WebGL Renderer] WARNING : Using Standard me.Text with WebGL will severly impact performances !"); // create the font texture if not done yet
+
+ this.createFontTexture(this.cache);
+ }
+
+ return this.fontContext2D;
+ },
+
+ /**
+ * scales the canvas & GL Context
+ * @name scaleCanvas
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ */
+ scaleCanvas: function scaleCanvas(scaleX, scaleY) {
+ var w = this.canvas.width * scaleX;
+ var h = this.canvas.height * scaleY; // adjust CSS style for High-DPI devices
+
+ if (me.device.devicePixelRatio > 1) {
+ this.canvas.style.width = w / me.device.devicePixelRatio + "px";
+ this.canvas.style.height = h / me.device.devicePixelRatio + "px";
+ } else {
+ this.canvas.style.width = w + "px";
+ this.canvas.style.height = h + "px";
+ }
+
+ this.compositor.setProjection(this.canvas.width, this.canvas.height);
+ },
+
+ /**
+ * restores the canvas context
+ * @name restore
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ */
+ restore: function restore() {
+ // do nothing if there is no saved states
+ if (this._matrixStack.length !== 0) {
+ var color = this._colorStack.pop();
+
+ var matrix = this._matrixStack.pop(); // restore the previous context
+
+
+ this.currentColor.copy(color);
+ this.currentTransform.copy(matrix); // recycle objects
+
+ me.pool.push(color);
+ me.pool.push(matrix);
+ }
+
+ if (this._scissorStack.length !== 0) {
+ // FIXME : prevent `scissor` object realloc and GC
+ this.currentScissor.set(this._scissorStack.pop());
+ } else {
+ // turn off scissor test
+ this.gl.disable(this.gl.SCISSOR_TEST);
+ this.currentScissor[0] = 0;
+ this.currentScissor[1] = 0;
+ this.currentScissor[2] = this.backBufferCanvas.width;
+ this.currentScissor[3] = this.backBufferCanvas.height;
+ }
+ },
+
+ /**
+ * saves the canvas context
+ * @name save
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ */
+ save: function save() {
+ this._colorStack.push(this.currentColor.clone());
+
+ this._matrixStack.push(this.currentTransform.clone());
+
+ if (this.gl.isEnabled(this.gl.SCISSOR_TEST)) {
+ // FIXME avoid slice and object realloc
+ this._scissorStack.push(this.currentScissor.slice());
+ }
+ },
+
+ /**
+ * rotates the uniform matrix
+ * @name rotate
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {Number} angle in radians
+ */
+ rotate: function rotate(angle) {
+ this.currentTransform.rotate(angle);
+ },
+
+ /**
+ * scales the uniform matrix
+ * @name scale
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {Number} x
+ * @param {Number} y
+ */
+ scale: function scale(x, y) {
+ this.currentTransform.scale(x, y);
+ },
+
+ /**
+ * not used by this renderer?
+ * @ignore
+ */
+ setAntiAlias: function setAntiAlias(context, enable) {
+ this._super(me.Renderer, "setAntiAlias", [context, enable]); // TODO: perhaps handle GLNEAREST or other options with texture binding
+
+ },
+
+ /**
+ * Set the global alpha
+ * @name setGlobalAlpha
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {Number} alpha 0.0 to 1.0 values accepted.
+ */
+ setGlobalAlpha: function setGlobalAlpha(a) {
+ this.currentColor.glArray[3] = a;
+ },
+
+ /**
+ * Set the current fill & stroke style color.
+ * By default, or upon reset, the value is set to #000000.
+ * @name setColor
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {me.Color|String} color css color string.
+ */
+ setColor: function setColor(color) {
+ var alpha = this.currentColor.glArray[3];
+ this.currentColor.copy(color);
+ this.currentColor.glArray[3] *= alpha;
+ },
+
+ /**
+ * Set the line width
+ * @name setLineWidth
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {Number} width Line width
+ */
+ setLineWidth: function setLineWidth(width) {
+ this.getScreenContext().lineWidth(width);
+ },
+
+ /**
+ * Stroke an arc at the specified coordinates with given radius, start and end points
+ * @name strokeArc
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {Number} x arc center point x-axis
+ * @param {Number} y arc center point y-axis
+ * @param {Number} radius
+ * @param {Number} start start angle in radians
+ * @param {Number} end end angle in radians
+ * @param {Boolean} [antiClockwise=false] draw arc anti-clockwise
+ */
+ strokeArc: function strokeArc(x, y, radius, start, end, antiClockwise, fill) {
+ if (fill === true) {
+ this.fillArc(x, y, radius, start, end, antiClockwise);
+ } else {
+ console.warn("strokeArc() is not implemented");
+ }
+ },
+
+ /**
+ * Fill an arc at the specified coordinates with given radius, start and end points
+ * @name fillArc
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {Number} x arc center point x-axis
+ * @param {Number} y arc center point y-axis
+ * @param {Number} radius
+ * @param {Number} start start angle in radians
+ * @param {Number} end end angle in radians
+ * @param {Boolean} [antiClockwise=false] draw arc anti-clockwise
+ */
+ fillArc: function fillArc(x, y, radius, start, end, antiClockwise) {
+ console.warn("fillArc() is not implemented");
+ },
+
+ /**
+ * Stroke an ellipse at the specified coordinates with given radius
+ * @name strokeEllipse
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {Number} x ellipse center point x-axis
+ * @param {Number} y ellipse center point y-axis
+ * @param {Number} w horizontal radius of the ellipse
+ * @param {Number} h vertical radius of the ellipse
+ */
+ strokeEllipse: function strokeEllipse(x, y, w, h, fill) {
+ if (fill === true) {
+ this.fillEllipse(x, y, w, h);
+ } else {
+ // XXX to be optimzed using a specific shader
+ var len = Math.floor(24 * Math.sqrt(w)) || Math.floor(12 * Math.sqrt(w + h));
+ var segment = me.Math.TAU / len;
+ var points = this._glPoints,
+ i; // Grow internal points buffer if necessary
+
+ for (i = points.length; i < len; i++) {
+ points.push(new me.Vector2d());
+ } // calculate and draw all segments
+
+
+ for (i = 0; i < len; i++) {
+ points[i].x = x + Math.sin(segment * -i) * w;
+ points[i].y = y + Math.cos(segment * -i) * h;
+ } // batch draw all lines
+
+
+ this.compositor.drawLine(points, len);
+ }
+ },
+
+ /**
+ * Fill an ellipse at the specified coordinates with given radius
+ * @name fillEllipse
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {Number} x ellipse center point x-axis
+ * @param {Number} y ellipse center point y-axis
+ * @param {Number} w horizontal radius of the ellipse
+ * @param {Number} h vertical radius of the ellipse
+ */
+ fillEllipse: function fillEllipse(x, y, w, h) {
+ // XXX to be optimzed using a specific shader
+ var len = Math.floor(24 * Math.sqrt(w)) || Math.floor(12 * Math.sqrt(w + h));
+ var segment = me.Math.TAU / len;
+ var points = this._glPoints;
+ var index = 0,
+ i; // Grow internal points buffer if necessary
+
+ for (i = points.length; i < (len + 1) * 2; i++) {
+ points.push(new me.Vector2d());
+ } // draw all vertices vertex coordinates
+
+
+ for (i = 0; i < len + 1; i++) {
+ points[index++].set(x, y);
+ points[index++].set(x + Math.sin(segment * i) * w, y + Math.cos(segment * i) * h);
+ } // batch draw all triangles
+
+
+ this.compositor.drawTriangle(points, index, true);
+ },
+
+ /**
+ * Stroke a line of the given two points
+ * @name strokeLine
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {Number} startX the start x coordinate
+ * @param {Number} startY the start y coordinate
+ * @param {Number} endX the end x coordinate
+ * @param {Number} endY the end y coordinate
+ */
+ strokeLine: function strokeLine(startX, startY, endX, endY) {
+ var points = this._glPoints;
+ points[0].x = startX;
+ points[0].y = startY;
+ points[1].x = endX;
+ points[1].y = endY;
+ this.compositor.drawLine(points, 2, true);
+ },
+
+ /**
+ * Fill a line of the given two points
+ * @name fillLine
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {Number} startX the start x coordinate
+ * @param {Number} startY the start y coordinate
+ * @param {Number} endX the end x coordinate
+ * @param {Number} endY the end y coordinate
+ */
+ fillLine: function fillLine(startX, startY, endX, endY) {
+ this.strokeLine(startX, startY, endX, endY);
+ },
+
+ /**
+ * Stroke a me.Polygon on the screen with a specified color
+ * @name strokePolygon
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {me.Polygon} poly the shape to draw
+ */
+ strokePolygon: function strokePolygon(poly, fill) {
+ if (fill === true) {
+ this.fillPolygon(poly);
+ } else {
+ var len = poly.points.length,
+ points = this._glPoints,
+ i; // Grow internal points buffer if necessary
+
+ for (i = points.length; i < len; i++) {
+ points.push(new me.Vector2d());
+ } // calculate and draw all segments
+
+
+ for (i = 0; i < len; i++) {
+ points[i].x = poly.pos.x + poly.points[i].x;
+ points[i].y = poly.pos.y + poly.points[i].y;
+ }
+
+ this.compositor.drawLine(points, len);
+ }
+ },
+
+ /**
+ * Fill a me.Polygon on the screen
+ * @name fillPolygon
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {me.Polygon} poly the shape to draw
+ */
+ fillPolygon: function fillPolygon(poly) {
+ var points = poly.points;
+ var glPoints = this._glPoints;
+ var indices = poly.getIndices();
+ var x = poly.pos.x,
+ y = poly.pos.y; // Grow internal points buffer if necessary
+
+ for (i = glPoints.length; i < indices.length; i++) {
+ glPoints.push(new me.Vector2d());
+ } // calculate all vertices
+
+
+ for (var i = 0; i < indices.length; i++) {
+ glPoints[i].set(x + points[indices[i]].x, y + points[indices[i]].y);
+ } // draw all triangle
+
+
+ this.compositor.drawTriangle(glPoints, indices.length);
+ },
+
+ /**
+ * Draw a stroke rectangle at the specified coordinates
+ * @name strokeRect
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} width
+ * @param {Number} height
+ */
+ strokeRect: function strokeRect(x, y, width, height) {
+ var points = this._glPoints;
+ points[0].x = x;
+ points[0].y = y;
+ points[1].x = x + width;
+ points[1].y = y;
+ points[2].x = x + width;
+ points[2].y = y + height;
+ points[3].x = x;
+ points[3].y = y + height;
+ this.compositor.drawLine(points, 4);
+ },
+
+ /**
+ * Draw a filled rectangle at the specified coordinates
+ * @name fillRect
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} width
+ * @param {Number} height
+ */
+ fillRect: function fillRect(x, y, width, height) {
+ var glPoints = this._glPoints;
+ glPoints[0].x = x + width;
+ glPoints[0].y = y;
+ glPoints[1].x = x;
+ glPoints[1].y = y;
+ glPoints[2].x = x + width;
+ glPoints[2].y = y + height;
+ glPoints[3].x = x;
+ glPoints[3].y = y + height;
+ this.compositor.drawTriangle(glPoints, 4, true);
+ },
+
+ /**
+ * Reset (overrides) the renderer transformation matrix to the
+ * identity one, and then apply the given transformation matrix.
+ * @name setTransform
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {me.Matrix2d} mat2d Matrix to transform by
+ */
+ setTransform: function setTransform(mat2d) {
+ this.resetTransform();
+ this.transform(mat2d);
+ },
+
+ /**
+ * Multiply given matrix into the renderer tranformation matrix
+ * @name transform
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {me.Matrix2d} mat2d Matrix to transform by
+ */
+ transform: function transform(mat2d) {
+ this.currentTransform.multiply(mat2d);
+
+ if (this.settings.subPixel === false) {
+ // snap position values to pixel grid
+ var a = this.currentTransform.val;
+ a[6] = ~~a[6];
+ a[7] = ~~a[7];
+ }
+ },
+
+ /**
+ * Translates the uniform matrix by the given coordinates
+ * @name translate
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {Number} x
+ * @param {Number} y
+ */
+ translate: function translate(x, y) {
+ if (this.settings.subPixel === false) {
+ this.currentTransform.translate(~~x, ~~y);
+ } else {
+ this.currentTransform.translate(x, y);
+ }
+ },
+
+ /**
+ * clip the given region from the original canvas. Once a region is clipped,
+ * all future drawing will be limited to the clipped region.
+ * You can however save the current region using the save(),
+ * and restore it (with the restore() method) any time in the future.
+ * (this is an experimental feature !)
+ * @name clipRect
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} width
+ * @param {Number} height
+ */
+ clipRect: function clipRect(x, y, width, height) {
+ var canvas = this.backBufferCanvas;
+ var gl = this.gl; // if requested box is different from the current canvas size
+
+ if (x !== 0 || y !== 0 || width !== canvas.width || height !== canvas.height) {
+ var currentScissor = this.currentScissor;
+
+ if (gl.isEnabled(gl.SCISSOR_TEST)) {
+ // if same as the current scissor box do nothing
+ if (currentScissor[0] === x && currentScissor[1] === y && currentScissor[2] === width && currentScissor[3] === height) {
+ return;
+ }
+ } // flush the compositor
+
+
+ this.flush(); // turn on scissor test
+
+ gl.enable(this.gl.SCISSOR_TEST); // set the scissor rectangle (note : coordinates are left/bottom)
+
+ gl.scissor( // scissor does not account for currentTransform, so manually adjust
+ x + this.currentTransform.tx, canvas.height - height - y - this.currentTransform.ty, width, height); // save the new currentScissor box
+
+ currentScissor[0] = x;
+ currentScissor[1] = y;
+ currentScissor[2] = width;
+ currentScissor[3] = height;
+ } else {
+ // turn off scissor test
+ gl.disable(gl.SCISSOR_TEST);
+ }
+ },
+
+ /**
+ * A mask limits rendering elements to the shape and position of the given mask object.
+ * So, if the renderable is larger than the mask, only the intersecting part of the renderable will be visible.
+ * Mask are not preserved through renderer context save and restore.
+ * @name setMask
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ * @param {me.Rect|me.Polygon|me.Line|me.Ellipse} [mask] the shape defining the mask to be applied
+ */
+ setMask: function setMask(mask) {
+ var gl = this.gl; // flush the compositor
+
+ this.flush(); // Enable and setup GL state to write to stencil buffer
+
+ gl.enable(gl.STENCIL_TEST);
+ gl.clear(gl.STENCIL_BUFFER_BIT);
+ gl.colorMask(false, false, false, false);
+ gl.stencilFunc(gl.NOTEQUAL, 1, 1);
+ gl.stencilOp(gl.REPLACE, gl.REPLACE, gl.REPLACE);
+ this.fill(mask); // flush the compositor
+
+ this.flush(); // Use stencil buffer to affect next rendering object
+
+ gl.colorMask(true, true, true, true);
+ gl.stencilFunc(gl.EQUAL, 1, 1);
+ gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
+ },
+
+ /**
+ * disable (remove) the rendering mask set through setMask.
+ * @name clearMask
+ * @see setMask
+ * @memberOf me.WebGLRenderer.prototype
+ * @function
+ */
+ clearMask: function clearMask() {
+ // flush the compositor
+ this.flush();
+ this.gl.disable(this.gl.STENCIL_TEST);
+ }
+ });
+ })();
+
+ (function () {
+ // Handy constants
+ var VERTEX_SIZE = 2;
+ var COLOR_SIZE = 4;
+ var TEXTURE_SIZE = 1;
+ var REGION_SIZE = 2;
+ var ELEMENT_SIZE = VERTEX_SIZE + COLOR_SIZE + TEXTURE_SIZE + REGION_SIZE;
+ var ELEMENT_OFFSET = ELEMENT_SIZE * Float32Array.BYTES_PER_ELEMENT;
+ var VERTEX_ELEMENT = 0;
+ var COLOR_ELEMENT = VERTEX_ELEMENT + VERTEX_SIZE;
+ var TEXTURE_ELEMENT = COLOR_ELEMENT + COLOR_SIZE;
+ var REGION_ELEMENT = TEXTURE_ELEMENT + TEXTURE_SIZE;
+ var VERTEX_OFFSET = VERTEX_ELEMENT * Float32Array.BYTES_PER_ELEMENT;
+ var COLOR_OFFSET = COLOR_ELEMENT * Float32Array.BYTES_PER_ELEMENT;
+ var TEXTURE_OFFSET = TEXTURE_ELEMENT * Float32Array.BYTES_PER_ELEMENT;
+ var REGION_OFFSET = REGION_ELEMENT * Float32Array.BYTES_PER_ELEMENT;
+ var ELEMENTS_PER_QUAD = 4;
+ var INDICES_PER_QUAD = 6;
+ var MAX_LENGTH = 16000;
+ /**
+ * A WebGL texture Compositor object. This class handles all of the WebGL state
+ * Pushes texture regions into WebGL buffers, automatically flushes to GPU
+ * @extends me.Object
+ * @namespace me.WebGLRenderer.Compositor
+ * @memberOf me
+ * @constructor
+ * @param {me.WebGLRenderer} renderer the current WebGL renderer session
+ */
+
+ me.WebGLRenderer.Compositor = me.Object.extend({
+ /**
+ * @ignore
+ */
+ init: function init(renderer) {
+ // local reference
+ var gl = renderer.gl;
+ /**
+ * The number of quads held in the batch
+ * @name length
+ * @memberOf me.WebGLRenderer.Compositor
+ * @type Number
+ * @readonly
+ */
+
+ this.length = 0; // Hash map of texture units
+
+ this.units = [];
+ /*
+ * XXX: The GLSL compiler pukes with "memory exhausted" when it is
+ * given long if-then-else chains.
+ *
+ * See: http://stackoverflow.com/questions/15828966/glsl-compile-error-memory-exhausted
+ *
+ * Workaround the problem by limiting the max texture support to 24.
+ * The magic number was determined by testing under different UAs.
+ * All Desktop UAs were capable of compiling with 27 fragment shader
+ * samplers. Using 24 seems like a reasonable compromise;
+ *
+ * 24 = 2^4 + 2^3
+ *
+ * As of October 2015, approximately 4.2% of all WebGL-enabled UAs
+ * support more than 24 max textures, according to
+ * http://webglstats.com/
+ */
+
+ this.maxTextures = Math.min(24, gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS)); // Vector pool
+
+ this.v = [new me.Vector2d(), new me.Vector2d(), new me.Vector2d(), new me.Vector2d()]; // the associated renderer
+ // TODO : add a set context or whatever function, and split
+ // the constructor accordingly, so that this is easier to restore
+ // the GL context when lost
+
+ this.renderer = renderer; // WebGL context
+
+ this.gl = renderer.gl; // Global transformation matrix
+
+ this.matrix = renderer.currentTransform; // Global fill color
+
+ this.color = renderer.currentColor; // Global tint color
+
+ this.tint = renderer.currentTint; // Uniform projection matrix
+
+ this.uMatrix = new me.Matrix2d(); // reference to the active shader
+
+ this.activeShader = null; // Load and create shader programs
+
+ this.primitiveShader = new me.PrimitiveGLShader(this.gl);
+ this.quadShader = new me.QuadGLShader(this.gl, this.maxTextures); // Stream buffer
+
+ this.sb = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.sb);
+ gl.bufferData(gl.ARRAY_BUFFER, MAX_LENGTH * ELEMENT_OFFSET * ELEMENTS_PER_QUAD, gl.STREAM_DRAW);
+ this.sbSize = 256;
+ this.sbIndex = 0; // Quad stream buffer
+
+ this.stream = new Float32Array(this.sbSize * ELEMENT_SIZE * ELEMENTS_PER_QUAD); // Index buffer
+
+ this.ib = gl.createBuffer();
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ib);
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.createIB(), gl.STATIC_DRAW); // Bind attribute pointers for quad shader
+
+ gl.vertexAttribPointer(this.quadShader.attributes.aVertex, VERTEX_SIZE, gl.FLOAT, false, ELEMENT_OFFSET, VERTEX_OFFSET);
+ gl.vertexAttribPointer(this.quadShader.attributes.aColor, COLOR_SIZE, gl.FLOAT, false, ELEMENT_OFFSET, COLOR_OFFSET);
+ gl.vertexAttribPointer(this.quadShader.attributes.aTexture, TEXTURE_SIZE, gl.FLOAT, false, ELEMENT_OFFSET, TEXTURE_OFFSET);
+ gl.vertexAttribPointer(this.quadShader.attributes.aRegion, REGION_SIZE, gl.FLOAT, false, ELEMENT_OFFSET, REGION_OFFSET);
+ this.reset();
+ this.setProjection(gl.canvas.width, gl.canvas.height); // Initialize clear color
+
+ gl.clearColor(0.0, 0.0, 0.0, 1.0);
+ },
+
+ /**
+ * Reset compositor internal state
+ * @ignore
+ */
+ reset: function reset() {
+ this.sbIndex = 0;
+ this.length = 0;
+ var samplers = []; // WebGL context
+
+ this.gl = this.renderer.gl;
+
+ for (var i = 0; i < this.maxTextures; i++) {
+ this.units[i] = false;
+ samplers[i] = i;
+ } // set the quad shader as the default program
+
+
+ this.useShader(this.quadShader);
+ this.quadShader.uniforms.uSampler = samplers;
+ },
+
+ /**
+ * Sets the projection matrix with the given size
+ * @name setProjection
+ * @memberOf me.WebGLRenderer.Compositor
+ * @function
+ * @param {Number} w WebGL Canvas width
+ * @param {Number} h WebGL Canvas height
+ */
+ setProjection: function setProjection(w, h) {
+ this.flush();
+ this.gl.viewport(0, 0, w, h);
+ this.uMatrix.setTransform(2 / w, 0, 0, 0, -2 / h, 0, -1, 1, 1);
+ },
+
+ /**
+ * Create a texture from an image
+ * @name createTexture
+ * @memberOf me.WebGLRenderer.Compositor
+ * @function
+ * @param {Number} unit Destination texture unit
+ * @param {Image|Canvas|ImageData|UInt8Array[]|Float32Array[]} image Source image
+ * @param {Number} filter gl.LINEAR or gl.NEAREST
+ * @param {String} [repeat="no-repeat"] Image repeat behavior (see {@link me.ImageLayer#repeat})
+ * @param {Number} [w] Source image width (Only use with UInt8Array[] or Float32Array[] source image)
+ * @param {Number} [h] Source image height (Only use with UInt8Array[] or Float32Array[] source image)
+ * @param {Number} [b] Source image border (Only use with UInt8Array[] or Float32Array[] source image)
+ * @param {Number} [b] Source image border (Only use with UInt8Array[] or Float32Array[] source image)
+ * @param {Boolean} [premultipliedAlpha=true] Multiplies the alpha channel into the other color channels
+ * @return {WebGLTexture} A texture object
+ */
+ createTexture: function createTexture(unit, image, filter, repeat, w, h, b, premultipliedAlpha) {
+ var gl = this.gl;
+ repeat = repeat || "no-repeat";
+ var isPOT = me.Math.isPowerOfTwo(w || image.width) && me.Math.isPowerOfTwo(h || image.height);
+ var texture = gl.createTexture();
+ var rs = repeat.search(/^repeat(-x)?$/) === 0 && isPOT ? gl.REPEAT : gl.CLAMP_TO_EDGE;
+ var rt = repeat.search(/^repeat(-y)?$/) === 0 && isPOT ? gl.REPEAT : gl.CLAMP_TO_EDGE;
+ gl.activeTexture(gl.TEXTURE0 + unit);
+ gl.bindTexture(gl.TEXTURE_2D, texture);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, rs);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, rt);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, typeof premultipliedAlpha === "boolean" ? premultipliedAlpha : true);
+
+ if (w || h || b) {
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, b, gl.RGBA, gl.UNSIGNED_BYTE, image);
+ } else {
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
+ }
+
+ return texture;
+ },
+
+ /**
+ * @ignore
+ */
+ uploadTexture: function uploadTexture(texture, w, h, b, force) {
+ var unit = this.renderer.cache.getUnit(texture);
+
+ if (!this.units[unit] || force) {
+ this.units[unit] = true;
+ this.createTexture(unit, texture.getTexture(), this.renderer.settings.antiAlias ? this.gl.LINEAR : this.gl.NEAREST, texture.repeat, w, h, b, texture.premultipliedAlpha);
+ }
+
+ return unit;
+ },
+
+ /**
+ * Create a full index buffer for the element array
+ * @ignore
+ */
+ createIB: function createIB() {
+ var indices = [0, 1, 2, 2, 1, 3]; // ~384KB index buffer
+
+ var data = new Array(MAX_LENGTH * INDICES_PER_QUAD);
+
+ for (var i = 0; i < data.length; i++) {
+ data[i] = indices[i % INDICES_PER_QUAD] + ~~(i / INDICES_PER_QUAD) * ELEMENTS_PER_QUAD;
+ }
+
+ return new Uint16Array(data);
+ },
+
+ /**
+ * Resize the stream buffer, retaining its original contents
+ * @ignore
+ */
+ resizeSB: function resizeSB() {
+ this.sbSize <<= 1;
+ var stream = new Float32Array(this.sbSize * ELEMENT_SIZE * ELEMENTS_PER_QUAD);
+ stream.set(this.stream);
+ this.stream = stream;
+ },
+
+ /**
+ * Select the shader to use for compositing
+ * @name useShader
+ * @see me.GLShader
+ * @memberOf me.WebGLRenderer.Compositor
+ * @function
+ * @param {me.GLShader} shader a reference to a GLShader instance
+ */
+ useShader: function useShader(shader) {
+ if (this.activeShader !== shader) {
+ this.flush();
+ this.activeShader = shader;
+ this.activeShader.bind();
+ this.activeShader.uniforms.uMatrix = this.uMatrix.val;
+ }
+ },
+
+ /**
+ * Add a textured quad
+ * @name addQuad
+ * @memberOf me.WebGLRenderer.Compositor
+ * @function
+ * @param {me.video.renderer.Texture} texture Source texture
+ * @param {String} key Source texture region name
+ * @param {Number} x Destination x-coordinate
+ * @param {Number} y Destination y-coordinate
+ * @param {Number} w Destination width
+ * @param {Number} h Destination height
+ */
+ addQuad: function addQuad(texture, key, x, y, w, h) {
+ var color = this.color.toGL();
+ var tint = this.tint.toGL();
+
+ if (color[3] < 1 / 255) {
+ // Fast path: don't send fully transparent quads
+ return;
+ } else {
+ // use the global alpha
+ tint[3] = color[3];
+ }
+
+ this.useShader(this.quadShader);
+
+ if (this.length >= MAX_LENGTH) {
+ this.flush();
+ }
+
+ if (this.length >= this.sbSize) {
+ this.resizeSB();
+ } // Transform vertices
+
+
+ var m = this.matrix,
+ v0 = this.v[0].set(x, y),
+ v1 = this.v[1].set(x + w, y),
+ v2 = this.v[2].set(x, y + h),
+ v3 = this.v[3].set(x + w, y + h);
+
+ if (!m.isIdentity()) {
+ m.multiplyVector(v0);
+ m.multiplyVector(v1);
+ m.multiplyVector(v2);
+ m.multiplyVector(v3);
+ } // Array index computation
+
+
+ var idx0 = this.sbIndex,
+ idx1 = idx0 + ELEMENT_SIZE,
+ idx2 = idx1 + ELEMENT_SIZE,
+ idx3 = idx2 + ELEMENT_SIZE; // Fill vertex buffer
+ // FIXME: Pack each vertex vector into single float
+
+ this.stream[idx0 + VERTEX_ELEMENT + 0] = v0.x;
+ this.stream[idx0 + VERTEX_ELEMENT + 1] = v0.y;
+ this.stream[idx1 + VERTEX_ELEMENT + 0] = v1.x;
+ this.stream[idx1 + VERTEX_ELEMENT + 1] = v1.y;
+ this.stream[idx2 + VERTEX_ELEMENT + 0] = v2.x;
+ this.stream[idx2 + VERTEX_ELEMENT + 1] = v2.y;
+ this.stream[idx3 + VERTEX_ELEMENT + 0] = v3.x;
+ this.stream[idx3 + VERTEX_ELEMENT + 1] = v3.y; // Fill color buffer
+ // FIXME: Pack color vector into single float
+
+ this.stream.set(tint, idx0 + COLOR_ELEMENT);
+ this.stream.set(tint, idx1 + COLOR_ELEMENT);
+ this.stream.set(tint, idx2 + COLOR_ELEMENT);
+ this.stream.set(tint, idx3 + COLOR_ELEMENT); // Fill texture index buffer
+ // FIXME: Can the texture index be packed into another element?
+
+ var unit = this.uploadTexture(texture);
+ this.stream[idx0 + TEXTURE_ELEMENT] = this.stream[idx1 + TEXTURE_ELEMENT] = this.stream[idx2 + TEXTURE_ELEMENT] = this.stream[idx3 + TEXTURE_ELEMENT] = unit; // Fill texture coordinates buffer
+
+ var uvs = texture.getUVs(key); // FIXME: Pack each texture coordinate into single floats
+
+ this.stream[idx0 + REGION_ELEMENT + 0] = uvs[0];
+ this.stream[idx0 + REGION_ELEMENT + 1] = uvs[1];
+ this.stream[idx1 + REGION_ELEMENT + 0] = uvs[2];
+ this.stream[idx1 + REGION_ELEMENT + 1] = uvs[1];
+ this.stream[idx2 + REGION_ELEMENT + 0] = uvs[0];
+ this.stream[idx2 + REGION_ELEMENT + 1] = uvs[3];
+ this.stream[idx3 + REGION_ELEMENT + 0] = uvs[2];
+ this.stream[idx3 + REGION_ELEMENT + 1] = uvs[3];
+ this.sbIndex += ELEMENT_SIZE * ELEMENTS_PER_QUAD;
+ this.length++;
+ },
+
+ /**
+ * Flush batched texture operations to the GPU
+ * @name flush
+ * @memberOf me.WebGLRenderer.Compositor
+ * @function
+ */
+ flush: function flush() {
+ if (this.length) {
+ var gl = this.gl; // Copy data into stream buffer
+
+ var len = this.length * ELEMENT_SIZE * ELEMENTS_PER_QUAD;
+ gl.bufferData(gl.ARRAY_BUFFER, this.stream.subarray(0, len), gl.STREAM_DRAW); // Draw the stream buffer
+
+ gl.drawElements(gl.TRIANGLES, this.length * INDICES_PER_QUAD, gl.UNSIGNED_SHORT, 0);
+ this.sbIndex = 0;
+ this.length = 0;
+ }
+ },
+
+ /**
+ * Draw triangle(s)
+ * @name drawTriangle
+ * @memberOf me.WebGLRenderer.Compositor
+ * @function
+ * @param {me.Vector2d[]} points vertices
+ * @param {Number} [len=points.length] amount of points defined in the points array
+ * @param {Boolean} [strip=false] Whether the array defines a serie of connected triangles, sharing vertices
+ */
+ drawTriangle: function drawTriangle(points, len, strip) {
+ var gl = this.gl;
+ len = len || points.length;
+ this.useShader(this.primitiveShader); // Put vertex data into the stream buffer
+
+ var j = 0;
+ var m = this.matrix;
+ var m_isIdentity = m.isIdentity();
+
+ for (var i = 0; i < points.length; i++) {
+ if (!m_isIdentity) {
+ m.multiplyVector(points[i]);
+ }
+
+ this.stream[j++] = points[i].x;
+ this.stream[j++] = points[i].y;
+ } // Set the line color
+
+
+ this.primitiveShader.uniforms.uColor = this.color.glArray; // Copy data into the stream buffer
+
+ gl.bufferData(gl.ARRAY_BUFFER, this.stream.subarray(0, len * 2), gl.STREAM_DRAW); // FIXME: Configure vertex attrib pointers in `useShader`
+
+ gl.vertexAttribPointer(this.primitiveShader.attributes.aVertex, VERTEX_SIZE, gl.FLOAT, false, 0, 0); // Draw the stream buffer
+
+ gl.drawArrays(strip === true ? gl.TRIANGLE_STRIP : gl.TRIANGLES, 0, len); // FIXME: Configure vertex attrib pointers in `useShader`
+
+ gl.vertexAttribPointer(this.quadShader.attributes.aVertex, VERTEX_SIZE, gl.FLOAT, false, ELEMENT_OFFSET, VERTEX_OFFSET);
+ gl.vertexAttribPointer(this.quadShader.attributes.aColor, COLOR_SIZE, gl.FLOAT, false, ELEMENT_OFFSET, COLOR_OFFSET);
+ gl.vertexAttribPointer(this.quadShader.attributes.aTexture, TEXTURE_SIZE, gl.FLOAT, false, ELEMENT_OFFSET, TEXTURE_OFFSET);
+ gl.vertexAttribPointer(this.quadShader.attributes.aRegion, REGION_SIZE, gl.FLOAT, false, ELEMENT_OFFSET, REGION_OFFSET);
+ },
+
+ /**
+ * Draw a line
+ * @name drawLine
+ * @memberOf me.WebGLRenderer.Compositor
+ * @function
+ * @param {me.Vector2d[]} points Line vertices
+ * @param {Number} [len=points.length] amount of points defined in the points array
+ * @param {Boolean} [open=false] Whether the line is open (true) or closed (false)
+ */
+ drawLine: function drawLine(points, len, open) {
+ var gl = this.gl;
+ len = len || points.length;
+ this.useShader(this.primitiveShader); // Put vertex data into the stream buffer
+
+ var j = 0;
+ var m = this.matrix;
+ var m_isIdentity = m.isIdentity();
+
+ for (var i = 0; i < points.length; i++) {
+ if (!m_isIdentity) {
+ m.multiplyVector(points[i]);
+ }
+
+ this.stream[j++] = points[i].x;
+ this.stream[j++] = points[i].y;
+ } // Set the line color
+
+
+ this.primitiveShader.uniforms.uColor = this.color.glArray; // Copy data into the stream buffer
+
+ gl.bufferData(gl.ARRAY_BUFFER, this.stream.subarray(0, len * 2), gl.STREAM_DRAW); // FIXME: Configure vertex attrib pointers in `useShader`
+
+ gl.vertexAttribPointer(this.primitiveShader.attributes.aVertex, VERTEX_SIZE, gl.FLOAT, false, 0, 0); // Draw the stream buffer
+
+ gl.drawArrays(open === true ? gl.LINE_STRIP : gl.LINE_LOOP, 0, len); // FIXME: Configure vertex attrib pointers in `useShader`
+
+ gl.vertexAttribPointer(this.quadShader.attributes.aVertex, VERTEX_SIZE, gl.FLOAT, false, ELEMENT_OFFSET, VERTEX_OFFSET);
+ gl.vertexAttribPointer(this.quadShader.attributes.aColor, COLOR_SIZE, gl.FLOAT, false, ELEMENT_OFFSET, COLOR_OFFSET);
+ gl.vertexAttribPointer(this.quadShader.attributes.aTexture, TEXTURE_SIZE, gl.FLOAT, false, ELEMENT_OFFSET, TEXTURE_OFFSET);
+ gl.vertexAttribPointer(this.quadShader.attributes.aRegion, REGION_SIZE, gl.FLOAT, false, ELEMENT_OFFSET, REGION_OFFSET);
+ },
+
+ /**
+ * Clear the frame buffer, flushes the composite operations and calls
+ * gl.clear()
+ * @name clear
+ * @memberOf me.WebGLRenderer.Compositor
+ * @function
+ */
+ clear: function clear() {
+ this.flush();
+ this.gl.clear(this.gl.COLOR_BUFFER_BIT);
+ }
+ });
+ })();
+
+ (function () {
+ /**
+ * @private
+ */
+ function extractUniforms(gl, shader) {
+ var uniforms = {},
+ uniRx = /uniform\s+(\w+)\s+(\w+)/g,
+ uniformsData = {},
+ descriptor = {},
+ locations = {},
+ match; // Detect all uniform names and types
+
+ [shader.vertex, shader.fragment].forEach(function (shader) {
+ while (match = uniRx.exec(shader)) {
+ uniformsData[match[2]] = match[1];
+ }
+ }); // Get uniform references
+
+ Object.keys(uniformsData).forEach(function (name) {
+ var type = uniformsData[name];
+ locations[name] = gl.getUniformLocation(shader.program, name);
+ descriptor[name] = {
+ "get": function (name) {
+ /**
+ * A getter for the uniform location
+ * @ignore
+ */
+ return function () {
+ return locations[name];
+ };
+ }(name),
+ "set": function (name, type, fn) {
+ if (type.indexOf("mat") === 0) {
+ /**
+ * A generic setter for uniform matrices
+ * @ignore
+ */
+ return function (val) {
+ gl[fn](locations[name], false, val);
+ };
+ } else {
+ /**
+ * A generic setter for uniform vectors
+ * @ignore
+ */
+ return function (val) {
+ var fnv = fn;
+
+ if (val.length && fn.substr(-1) !== "v") {
+ fnv += "v";
+ }
+
+ gl[fnv](locations[name], val);
+ };
+ }
+ }(name, type, "uniform" + fnHash[type])
+ };
+ });
+ Object.defineProperties(uniforms, descriptor);
+ return uniforms;
+ }
+ /**
+ * @private
+ */
+
+ function extractAttributes(gl, shader) {
+ var attributes = {},
+ attrRx = /attribute\s+\w+\s+(\w+)/g,
+ attrData = [],
+ match; // Detect all attribute names
+
+ while (match = attrRx.exec(shader.vertex)) {
+ attrData.push(match[1]);
+ } // Get attribute references
+
+
+ attrData.forEach(function (attr) {
+ attributes[attr] = gl.getAttribLocation(shader.program, attr);
+ gl.enableVertexAttribArray(attributes[attr]);
+ });
+ return attributes;
+ }
+ /**
+ * @private
+ */
+
+ function compileShader(gl, type, source) {
+ var shader = gl.createShader(type);
+ gl.shaderSource(shader, source);
+ gl.compileShader(shader);
+
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+ throw new Error(gl.getShaderInfoLog(shader));
+ }
+
+ return shader;
+ }
+ /**
+ * Compile GLSL into a shader object
+ * @private
+ */
+
+ function compileProgram(gl, vertex, fragment) {
+ var vertShader = compileShader(gl, gl.VERTEX_SHADER, vertex);
+ var fragShader = compileShader(gl, gl.FRAGMENT_SHADER, fragment);
+ var program = gl.createProgram();
+ gl.attachShader(program, vertShader);
+ gl.attachShader(program, fragShader);
+ gl.linkProgram(program);
+
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
+ throw new Error("Error initializing Shader " + this + "\n" + "gl.VALIDATE_STATUS: " + gl.getProgramParameter(program, gl.VALIDATE_STATUS) + "\n" + "gl.getError()" + gl.getError() + "\n" + "gl.getProgramInfoLog()" + gl.getProgramInfoLog(program));
+ }
+
+ gl.useProgram(program); // clean-up
+
+ gl.deleteShader(vertShader);
+ gl.deleteShader(fragShader);
+ return program;
+ }
+ /**
+ * Hash map of GLSL data types to WebGL Uniform methods
+ * @private
+ */
+
+ var fnHash = {
+ "bool": "1i",
+ "int": "1i",
+ "float": "1f",
+ "vec2": "2fv",
+ "vec3": "3fv",
+ "vec4": "4fv",
+ "bvec2": "2iv",
+ "bvec3": "3iv",
+ "bvec4": "4iv",
+ "ivec2": "2iv",
+ "ivec3": "3iv",
+ "ivec4": "4iv",
+ "mat2": "Matrix2fv",
+ "mat3": "Matrix3fv",
+ "mat4": "Matrix4fv",
+ "sampler2D": "1i"
+ };
+ /**
+ * set precision for the fiven shader source
+ * won't don anyhing if the precision is already specified
+ * @private
+ */
+
+ function setPrecision(src, precision) {
+ if (src.substring(0, 9) !== "precision") {
+ return "precision " + precision + " float;" + src;
+ }
+
+ return src;
+ }
+ /**
+ * clean the given source from space, comments, etc...
+ * @private
+ */
+
+ function minify(src) {
+ // remove comments
+ src = src.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1"); // Remove leading and trailing whitespace from lines
+
+ src = src.replace(/(\\n\s+)|(\s+\\n)/g, ""); // Remove line breaks
+
+ src = src.replace(/(\\r|\\n)+/g, ""); // Remove unnecessary whitespace
+
+ src = src.replace(/\s*([;,[\](){}\\\/\-+*|^&!=<>?~%])\s*/g, "$1");
+ return src;
+ }
+ /**
+ * a base GL Shader object
+ * @class
+ * @extends me.Object
+ * @param {WebGLRenderingContext} gl the current WebGL rendering context
+ * @param {String} vertex a string containing the GLSL source code to set
+ * @param {String} fragment a string containing the GLSL source code to set
+ * @param {String} [precision=auto detected] float precision ('lowp', 'mediump' or 'highp').
+ * @constructor
+ * @see https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_on_the_web/GLSL_Shaders
+ * @example
+ * // create a basic shader
+ * var myShader = new me.GLShader(
+ * // WebGL rendering context
+ * gl,
+ * // vertex shader
+ * [
+ * "void main() {",
+ * " gl_Position = doMathToMakeClipspaceCoordinates;",
+ * "}"
+ * ].join("\n"),
+ * // fragment shader
+ * [
+ * "void main() {",
+ * " gl_FragColor = doMathToMakeAColor;",
+ * "}"
+ * ].join("\n")
+ * )
+ * // use the shader
+ * myShader.bind();
+ */
+
+ me.GLShader = me.Object.extend({
+ /**
+ * @ignore
+ */
+ init: function init(gl, vertex, fragment, precision) {
+ /**
+ * the active gl rendering context
+ * @public
+ * @type {WebGLRenderingContext}
+ * @name gl
+ * @memberOf me.GLShader
+ */
+ this.gl = gl;
+ /**
+ * the vertex shader source code
+ * @public
+ * @type {String}
+ * @name vertex
+ * @memberOf me.GLShader
+ */
+
+ this.vertex = setPrecision(minify(vertex), precision || me.device.getMaxShaderPrecision(this.gl));
+ /**
+ * the fragment shader source code
+ * @public
+ * @type {String}
+ * @name vertex
+ * @memberOf me.GLShader
+ */
+
+ this.fragment = setPrecision(minify(fragment), precision || me.device.getMaxShaderPrecision(this.gl));
+ /**
+ * a reference to the shader program (once compiled)
+ * @public
+ * @type {WebGLProgram}
+ * @name program
+ * @memberOf me.GLShader
+ */
+
+ this.program = compileProgram(this.gl, this.vertex, this.fragment);
+ /**
+ * the attributes of the shader
+ * @public
+ * @type {Object}
+ * @name attributes
+ * @memberOf me.GLShader
+ */
+
+ this.attributes = extractAttributes(this.gl, this);
+ /**
+ * the uniforms of the shader
+ * @public
+ * @type {Object}
+ * @name uniforms
+ * @memberOf me.GLShader
+ */
+
+ this.uniforms = extractUniforms(this.gl, this); // destroy the shader on context lost (will be recreated on context restore)
+
+ me.event.subscribe(me.event.WEBGL_ONCONTEXT_LOST, this.destroy.bind(this));
+ return this;
+ },
+
+ /**
+ * Installs this shader program as part of current rendering state
+ * @name bind
+ * @memberOf me.GLShader
+ * @function
+ */
+ bind: function bind() {
+ this.gl.useProgram(this.program);
+ },
+
+ /**
+ * destroy this shader objects resources (program, attributes, uniforms)
+ * @name destroy
+ * @memberOf me.GLShader
+ * @function
+ */
+ destroy: function destroy() {
+ this.uniforms = null;
+ this.attributes = null;
+ this.gl.deleteProgram(this.program);
+ this.vertex = null;
+ this.fragment = null;
+ }
+ });
+ })();
+
+ (function () {
+ /**
+ * a built-in shader used by the Compositor for primitive drawing
+ * @class
+ * @extends me.GLShader
+ * @see me.WebGLRenderer.Compositor
+ * @constructor
+ * @param {WebGLRenderingContext} gl the current WebGL rendering context
+ */
+ me.PrimitiveGLShader = me.GLShader.extend({
+ /**
+ * @ignore
+ */
+ init: function init(gl) {
+ this._super(me.GLShader, "init", [gl, [// vertex`
+ "precision highp float;", "// Current vertex point", "attribute vec2 aVertex;", "// Projection matrix", "uniform mat3 uMatrix;", "// Vertex color", "uniform vec4 uColor;", "// Fragment color", "varying vec4 vColor;", "void main(void) {", " // Transform the vertex position by the projection matrix", " gl_Position = vec4((uMatrix * vec3(aVertex, 1)).xy, 0, 1);", " // Pass the remaining attributes to the fragment shader", " vColor = vec4(uColor.rgb * uColor.a, uColor.a);", "}"].join("\n"), [// fragment
+ "// fragment color", "varying vec4 vColor;", "void main(void) {", " gl_FragColor = vColor;", "}"].join("\n")]);
+
+ return this;
+ }
+ });
+ })();
+
+ (function () {
+ /**
+ * a built-in shader used by the Compositor for Quad Texture drawing
+ * @class
+ * @extends me.GLShader
+ * @see me.WebGLRenderer.Compositor
+ * @constructor
+ * @param {WebGLRenderingContext} gl the current WebGL rendering context
+ * @param {Number} maxTextures the maximum amount of Texture supported by the WebGL Driver
+ */
+ me.QuadGLShader = me.GLShader.extend({
+ /**
+ * @ignore
+ */
+ init: function init(gl, maxTextures) {
+ this._super(me.GLShader, "init", [gl, [// vertex`
+ "precision highp float;", "attribute vec2 aVertex;", "attribute vec4 aColor;", "attribute float aTexture;", "attribute vec2 aRegion;", "uniform mat3 uMatrix;", "varying vec4 vColor;", "varying float vTexture;", "varying vec2 vRegion;", "void main(void) {", " // Transform the vertex position by the projection matrix", " gl_Position = vec4((uMatrix * vec3(aVertex, 1)).xy, 0, 1);", " // Pass the remaining attributes to the fragment shader", " vColor = vec4(aColor.rgb * aColor.a, aColor.a);", " vTexture = aTexture;", " vRegion = aRegion;", "}"].join("\n"), [// fragment
+
+ /*
+ * Dynamically indexing arrays in a fragment shader is not allowed:
+ *
+ * https://www.khronos.org/registry/webgl/specs/1.0/#4.3
+ *
+ * "
+ * Appendix A mandates certain forms of indexing of arrays; for example,
+ * within fragment shaders, indexing is only mandated with a
+ * constant-index-expression (see [GLES20GLSL] for the definition of this
+ * term). In the WebGL API, only the forms of indexing mandated in
+ * Appendix A are supported.
+ * "
+ *
+ * And GLES20GLSL has this to say about constant-index-expressions:
+ *
+ * "
+ * constant-index-expressions are a superset of constant-expressions.
+ * Constant-index-expressions can include loop indices as defined in
+ * Appendix A section 4.
+ *
+ * The following are constant-index-expressions:
+ * * Constant expressions
+ * * Loop indices as defined in section 4
+ * * Expressions composed of both of the above
+ * "
+ *
+ * To workaround this issue, we create a long if-then-else statement using
+ * a template processor; the number of branches depends only on the total
+ * number of texture units supported by the WebGL implementation.
+ *
+ * The number of available texture units is at least 8, but can be as high
+ * as 32 (as of 2016-01); source: http://webglstats.com/
+ * See: MAX_TEXTURE_IMAGE_UNITS
+ *
+ * The idea of sampler selection originated from work by Kenneth Russell and
+ * Nat Duca from the Chromium Team.
+ * See: http://webglsamples.org/sprites/readme.html
+ */
+ "uniform sampler2D uSampler[" + maxTextures + "];", "varying vec4 vColor;", "varying float vTexture;", "varying vec2 vRegion;", "void main(void) {", " // Convert texture unit index to integer", " int texture = int(vTexture);", " if (texture == 0) {", " gl_FragColor = texture2D(uSampler[0], vRegion) * vColor;", " }", " else {", " for (int i = 1; i < " + (maxTextures - 1) + "; i++) {", " if (texture == i) {", " gl_FragColor = texture2D(uSampler[i], vRegion) * vColor;", " return;", " }", " gl_FragColor = texture2D(uSampler[" + (maxTextures - 1) + "], vRegion) * vColor;", " };", " }", "}"].join("\n")]);
+
+ return this;
+ }
+ });
+ })();
+
+ (function () {
+ /**
+ * @namespace me.input
+ * @memberOf me
+ */
+ me.input = function () {
+ // hold public stuff in our singleton
+ var api = {};
+ /*
+ * PRIVATE STUFF
+ */
+
+ /**
+ * prevent event propagation
+ * @ignore
+ */
+
+ api._preventDefaultFn = function (e) {
+ // stop event propagation
+ if (e.stopPropagation) {
+ e.stopPropagation();
+ } else {
+ e.cancelBubble = true;
+ } // stop event default processing
+
+
+ if (e.preventDefault) {
+ e.preventDefault();
+ } else {
+ e.returnValue = false;
+ }
+
+ return false;
+ };
+ /*
+ * PUBLIC STUFF
+ */
+
+ /**
+ * Global flag to specify if melonJS should prevent all default browser action on registered events.
+ * default : true
+ * @public
+ * @type Boolean
+ * @name preventDefault
+ * @memberOf me.input
+ */
+
+
+ api.preventDefault = true; // return our object
+
+ return api;
+ }();
+ })();
+
+ (function (api) {
+ /*
+ * PRIVATE STUFF
+ */
+ // list of binded keys
+ api._KeyBinding = {}; // corresponding actions
+
+ var keyStatus = {}; // lock enable flag for keys
+
+ var keyLock = {}; // actual lock status of each key
+
+ var keyLocked = {}; // List of binded keys being held
+
+ var keyRefs = {}; // whether default event should be prevented for a given keypress
+
+ var preventDefaultForKeys = {}; // some useful flags
+
+ var keyboardInitialized = false;
+ /**
+ * enable keyboard event
+ * @ignore
+ */
+
+ api._enableKeyboardEvent = function () {
+ if (!keyboardInitialized) {
+ window.addEventListener("keydown", api._keydown, false);
+ window.addEventListener("keyup", api._keyup, false);
+ keyboardInitialized = true;
+ }
+ };
+ /**
+ * key down event
+ * @ignore
+ */
+
+
+ api._keydown = function (e, keyCode, mouseButton) {
+ keyCode = keyCode || e.keyCode || e.button;
+ var action = api._KeyBinding[keyCode]; // publish a message for keydown event
+
+ me.event.publish(me.event.KEYDOWN, [action, keyCode, action ? !keyLocked[action] : true]);
+
+ if (action) {
+ if (!keyLocked[action]) {
+ var trigger = typeof mouseButton !== "undefined" ? mouseButton : keyCode;
+
+ if (!keyRefs[action][trigger]) {
+ keyStatus[action]++;
+ keyRefs[action][trigger] = true;
+ }
+ } // prevent event propagation
+
+
+ if (preventDefaultForKeys[keyCode]) {
+ return api._preventDefaultFn(e);
+ } else {
+ return true;
+ }
+ }
+
+ return true;
+ };
+ /**
+ * key up event
+ * @ignore
+ */
+
+
+ api._keyup = function (e, keyCode, mouseButton) {
+ keyCode = keyCode || e.keyCode || e.button;
+ var action = api._KeyBinding[keyCode]; // publish a message for keydown event
+
+ me.event.publish(me.event.KEYUP, [action, keyCode]);
+
+ if (action) {
+ var trigger = typeof mouseButton !== "undefined" ? mouseButton : keyCode;
+ keyRefs[action][trigger] = undefined;
+
+ if (keyStatus[action] > 0) {
+ keyStatus[action]--;
+ }
+
+ keyLocked[action] = false; // prevent event propagation
+
+ if (preventDefaultForKeys[keyCode]) {
+ return api._preventDefaultFn(e);
+ } else {
+ return true;
+ }
+ }
+
+ return true;
+ };
+ /*
+ * PUBLIC STUFF
+ */
+
+ /**
+ * standard keyboard constants
+ * @public
+ * @enum {number}
+ * @namespace KEY
+ * @memberOf me.input
+ */
+
+
+ api.KEY = {
+ /** @memberOf me.input.KEY */
+ "BACKSPACE": 8,
+
+ /** @memberOf me.input.KEY */
+ "TAB": 9,
+
+ /** @memberOf me.input.KEY */
+ "ENTER": 13,
+
+ /** @memberOf me.input.KEY */
+ "SHIFT": 16,
+
+ /** @memberOf me.input.KEY */
+ "CTRL": 17,
+
+ /** @memberOf me.input.KEY */
+ "ALT": 18,
+
+ /** @memberOf me.input.KEY */
+ "PAUSE": 19,
+
+ /** @memberOf me.input.KEY */
+ "CAPS_LOCK": 20,
+
+ /** @memberOf me.input.KEY */
+ "ESC": 27,
+
+ /** @memberOf me.input.KEY */
+ "SPACE": 32,
+
+ /** @memberOf me.input.KEY */
+ "PAGE_UP": 33,
+
+ /** @memberOf me.input.KEY */
+ "PAGE_DOWN": 34,
+
+ /** @memberOf me.input.KEY */
+ "END": 35,
+
+ /** @memberOf me.input.KEY */
+ "HOME": 36,
+
+ /** @memberOf me.input.KEY */
+ "LEFT": 37,
+
+ /** @memberOf me.input.KEY */
+ "UP": 38,
+
+ /** @memberOf me.input.KEY */
+ "RIGHT": 39,
+
+ /** @memberOf me.input.KEY */
+ "DOWN": 40,
+
+ /** @memberOf me.input.KEY */
+ "PRINT_SCREEN": 42,
+
+ /** @memberOf me.input.KEY */
+ "INSERT": 45,
+
+ /** @memberOf me.input.KEY */
+ "DELETE": 46,
+
+ /** @memberOf me.input.KEY */
+ "NUM0": 48,
+
+ /** @memberOf me.input.KEY */
+ "NUM1": 49,
+
+ /** @memberOf me.input.KEY */
+ "NUM2": 50,
+
+ /** @memberOf me.input.KEY */
+ "NUM3": 51,
+
+ /** @memberOf me.input.KEY */
+ "NUM4": 52,
+
+ /** @memberOf me.input.KEY */
+ "NUM5": 53,
+
+ /** @memberOf me.input.KEY */
+ "NUM6": 54,
+
+ /** @memberOf me.input.KEY */
+ "NUM7": 55,
+
+ /** @memberOf me.input.KEY */
+ "NUM8": 56,
+
+ /** @memberOf me.input.KEY */
+ "NUM9": 57,
+
+ /** @memberOf me.input.KEY */
+ "A": 65,
+
+ /** @memberOf me.input.KEY */
+ "B": 66,
+
+ /** @memberOf me.input.KEY */
+ "C": 67,
+
+ /** @memberOf me.input.KEY */
+ "D": 68,
+
+ /** @memberOf me.input.KEY */
+ "E": 69,
+
+ /** @memberOf me.input.KEY */
+ "F": 70,
+
+ /** @memberOf me.input.KEY */
+ "G": 71,
+
+ /** @memberOf me.input.KEY */
+ "H": 72,
+
+ /** @memberOf me.input.KEY */
+ "I": 73,
+
+ /** @memberOf me.input.KEY */
+ "J": 74,
+
+ /** @memberOf me.input.KEY */
+ "K": 75,
+
+ /** @memberOf me.input.KEY */
+ "L": 76,
+
+ /** @memberOf me.input.KEY */
+ "M": 77,
+
+ /** @memberOf me.input.KEY */
+ "N": 78,
+
+ /** @memberOf me.input.KEY */
+ "O": 79,
+
+ /** @memberOf me.input.KEY */
+ "P": 80,
+
+ /** @memberOf me.input.KEY */
+ "Q": 81,
+
+ /** @memberOf me.input.KEY */
+ "R": 82,
+
+ /** @memberOf me.input.KEY */
+ "S": 83,
+
+ /** @memberOf me.input.KEY */
+ "T": 84,
+
+ /** @memberOf me.input.KEY */
+ "U": 85,
+
+ /** @memberOf me.input.KEY */
+ "V": 86,
+
+ /** @memberOf me.input.KEY */
+ "W": 87,
+
+ /** @memberOf me.input.KEY */
+ "X": 88,
+
+ /** @memberOf me.input.KEY */
+ "Y": 89,
+
+ /** @memberOf me.input.KEY */
+ "Z": 90,
+
+ /** @memberOf me.input.KEY */
+ "WINDOW_KEY": 91,
+
+ /** @memberOf me.input.KEY */
+ "NUMPAD0": 96,
+
+ /** @memberOf me.input.KEY */
+ "NUMPAD1": 97,
+
+ /** @memberOf me.input.KEY */
+ "NUMPAD2": 98,
+
+ /** @memberOf me.input.KEY */
+ "NUMPAD3": 99,
+
+ /** @memberOf me.input.KEY */
+ "NUMPAD4": 100,
+
+ /** @memberOf me.input.KEY */
+ "NUMPAD5": 101,
+
+ /** @memberOf me.input.KEY */
+ "NUMPAD6": 102,
+
+ /** @memberOf me.input.KEY */
+ "NUMPAD7": 103,
+
+ /** @memberOf me.input.KEY */
+ "NUMPAD8": 104,
+
+ /** @memberOf me.input.KEY */
+ "NUMPAD9": 105,
+
+ /** @memberOf me.input.KEY */
+ "MULTIPLY": 106,
+
+ /** @memberOf me.input.KEY */
+ "ADD": 107,
+
+ /** @memberOf me.input.KEY */
+ "SUBSTRACT": 109,
+
+ /** @memberOf me.input.KEY */
+ "DECIMAL": 110,
+
+ /** @memberOf me.input.KEY */
+ "DIVIDE": 111,
+
+ /** @memberOf me.input.KEY */
+ "F1": 112,
+
+ /** @memberOf me.input.KEY */
+ "F2": 113,
+
+ /** @memberOf me.input.KEY */
+ "F3": 114,
+
+ /** @memberOf me.input.KEY */
+ "F4": 115,
+
+ /** @memberOf me.input.KEY */
+ "F5": 116,
+
+ /** @memberOf me.input.KEY */
+ "F6": 117,
+
+ /** @memberOf me.input.KEY */
+ "F7": 118,
+
+ /** @memberOf me.input.KEY */
+ "F8": 119,
+
+ /** @memberOf me.input.KEY */
+ "F9": 120,
+
+ /** @memberOf me.input.KEY */
+ "F10": 121,
+
+ /** @memberOf me.input.KEY */
+ "F11": 122,
+
+ /** @memberOf me.input.KEY */
+ "F12": 123,
+
+ /** @memberOf me.input.KEY */
+ "TILDE": 126,
+
+ /** @memberOf me.input.KEY */
+ "NUM_LOCK": 144,
+
+ /** @memberOf me.input.KEY */
+ "SCROLL_LOCK": 145,
+
+ /** @memberOf me.input.KEY */
+ "SEMICOLON": 186,
+
+ /** @memberOf me.input.KEY */
+ "PLUS": 187,
+
+ /** @memberOf me.input.KEY */
+ "COMMA": 188,
+
+ /** @memberOf me.input.KEY */
+ "MINUS": 189,
+
+ /** @memberOf me.input.KEY */
+ "PERIOD": 190,
+
+ /** @memberOf me.input.KEY */
+ "FORWAND_SLASH": 191,
+
+ /** @memberOf me.input.KEY */
+ "GRAVE_ACCENT": 192,
+
+ /** @memberOf me.input.KEY */
+ "OPEN_BRACKET": 219,
+
+ /** @memberOf me.input.KEY */
+ "BACK_SLASH": 220,
+
+ /** @memberOf me.input.KEY */
+ "CLOSE_BRACKET": 221,
+
+ /** @memberOf me.input.KEY */
+ "SINGLE_QUOTE": 222
+ };
+ /**
+ * return the key press status of the specified action
+ * @name isKeyPressed
+ * @memberOf me.input
+ * @public
+ * @function
+ * @param {String} action user defined corresponding action
+ * @return {Boolean} true if pressed
+ * @example
+ * if (me.input.isKeyPressed('left'))
+ * {
+ * //do something
+ * }
+ * else if (me.input.isKeyPressed('right'))
+ * {
+ * //do something else...
+ * }
+ *
+ */
+
+ api.isKeyPressed = function (action) {
+ if (keyStatus[action] && !keyLocked[action]) {
+ if (keyLock[action]) {
+ keyLocked[action] = true;
+ }
+
+ return true;
+ }
+
+ return false;
+ };
+ /**
+ * return the key status of the specified action
+ * @name keyStatus
+ * @memberOf me.input
+ * @public
+ * @function
+ * @param {String} action user defined corresponding action
+ * @return {Boolean} down (true) or up(false)
+ */
+
+
+ api.keyStatus = function (action) {
+ return keyStatus[action] > 0;
+ };
+ /**
+ * trigger the specified key (simulated) event
+ * @name triggerKeyEvent
+ * @memberOf me.input
+ * @public
+ * @function
+ * @param {me.input.KEY} keycode
+ * @param {Boolean} [status=false] true to trigger a key press, or false for key release
+ * @example
+ * // trigger a key press
+ * me.input.triggerKeyEvent(me.input.KEY.LEFT, true);
+ */
+
+
+ api.triggerKeyEvent = function (keycode, status) {
+ if (status) {
+ api._keydown({}, keycode);
+ } else {
+ api._keyup({}, keycode);
+ }
+ };
+ /**
+ * associate a user defined action to a keycode
+ * @name bindKey
+ * @memberOf me.input
+ * @public
+ * @function
+ * @param {me.input.KEY} keycode
+ * @param {String} action user defined corresponding action
+ * @param {Boolean} [lock=false] cancel the keypress event once read
+ * @param {Boolean} [preventDefault=me.input.preventDefault] prevent default browser action
+ * @example
+ * // enable the keyboard
+ * me.input.bindKey(me.input.KEY.LEFT, "left");
+ * me.input.bindKey(me.input.KEY.RIGHT, "right");
+ * me.input.bindKey(me.input.KEY.X, "jump", true);
+ * me.input.bindKey(me.input.KEY.F1, "options", true, true);
+ */
+
+
+ api.bindKey = function (keycode, action, lock, preventDefault) {
+ // make sure the keyboard is enable
+ api._enableKeyboardEvent();
+
+ if (typeof preventDefault !== "boolean") {
+ preventDefault = api.preventDefault;
+ }
+
+ api._KeyBinding[keycode] = action;
+ preventDefaultForKeys[keycode] = preventDefault;
+ keyStatus[action] = 0;
+ keyLock[action] = lock ? lock : false;
+ keyLocked[action] = false;
+ keyRefs[action] = {};
+ };
+ /**
+ * unlock a key manually
+ * @name unlockKey
+ * @memberOf me.input
+ * @public
+ * @function
+ * @param {String} action user defined corresponding action
+ * @example
+ * // Unlock jump when touching the ground
+ * if (!this.falling && !this.jumping) {
+ * me.input.unlockKey("jump");
+ * }
+ */
+
+
+ api.unlockKey = function (action) {
+ keyLocked[action] = false;
+ };
+ /**
+ * unbind the defined keycode
+ * @name unbindKey
+ * @memberOf me.input
+ * @public
+ * @function
+ * @param {me.input.KEY} keycode
+ * @example
+ * me.input.unbindKey(me.input.KEY.LEFT);
+ */
+
+
+ api.unbindKey = function (keycode) {
+ // clear the event status
+ var keybinding = api._KeyBinding[keycode];
+ keyStatus[keybinding] = 0;
+ keyLock[keybinding] = false;
+ keyRefs[keybinding] = {}; // remove the key binding
+
+ api._KeyBinding[keycode] = null;
+ preventDefaultForKeys[keycode] = null;
+ };
+ })(me.input);
+
+ (function () {
+ /**
+ * cache value for the offset of the canvas position within the page
+ * @ignore
+ */
+ var viewportOffset = new me.Vector2d();
+ /**
+ * a pointer object, representing a single finger on a touch enabled device.
+ * @class
+ * @extends me.Rect
+ * @memberOf me
+ * @constructor
+ */
+
+ me.Pointer = me.Rect.extend({
+ /**
+ * @ignore
+ */
+ init: function init(x, y, w, h) {
+ /**
+ * the originating Event Object
+ * @public
+ * @type {PointerEvent|TouchEvent|MouseEvent}
+ * @name event
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
+ * @memberOf me.Pointer
+ */
+ this.event = undefined;
+ /**
+ * a string containing the event's type.
+ * @public
+ * @type {String}
+ * @name type
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Event/type
+ * @memberOf me.Pointer
+ */
+
+ this.type = undefined;
+ /**
+ * the button property indicates which button was pressed on the mouse to trigger the event.
+ * @public
+ * @type {Number}
+ * @name button
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
+ * @memberOf me.Pointer
+ */
+
+ this.button = 0;
+ /**
+ * indicates whether or not the pointer device that created the event is the primary pointer.
+ * @public
+ * @type {Boolean}
+ * @name isPrimary
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/isPrimary
+ * @memberOf me.Pointer
+ */
+
+ this.isPrimary = false;
+ /**
+ * the horizontal coordinate at which the event occurred, relative to the left edge of the entire document.
+ * @public
+ * @type {Number}
+ * @name pageX
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/pageX
+ * @memberOf me.Pointer
+ */
+
+ this.pageX = 0;
+ /**
+ * the vertical coordinate at which the event occurred, relative to the left edge of the entire document.
+ * @public
+ * @type {Number}
+ * @name pageY
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/pageY
+ * @memberOf me.Pointer
+ */
+
+ this.pageY = 0;
+ /**
+ * the horizontal coordinate within the application's client area at which the event occurred
+ * @public
+ * @type {Number}
+ * @name clientX
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientX
+ * @memberOf me.Pointer
+ */
+
+ this.clientX = 0;
+ /**
+ * the vertical coordinate within the application's client area at which the event occurred
+ * @public
+ * @type {Number}
+ * @name clientY
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientY
+ * @memberOf me.Pointer
+ */
+
+ this.clientY = 0;
+ /**
+ * Event normalized X coordinate within the game canvas itself
+ *
+ * @public
+ * @type {Number}
+ * @name gameX
+ * @memberOf me.Pointer
+ */
+
+ this.gameX = 0;
+ /**
+ * Event normalized Y coordinate within the game canvas itself
+ *
+ * @public
+ * @type {Number}
+ * @name gameY
+ * @memberOf me.Pointer
+ */
+
+ this.gameY = 0;
+ /**
+ * Event X coordinate relative to the viewport
+ * @public
+ * @type {Number}
+ * @name gameScreenX
+ * @memberOf me.Pointer
+ */
+
+ this.gameScreenX = 0;
+ /**
+ * Event Y coordinate relative to the viewport
+ * @public
+ * @type {Number}
+ * @name gameScreenY
+ * @memberOf me.Pointer
+ */
+
+ this.gameScreenY = 0;
+ /**
+ * Event X coordinate relative to the map
+ * @public
+ * @type {Number}
+ * @name gameWorldX
+ * @memberOf me.Pointer
+ */
+
+ this.gameWorldX = 0;
+ /**
+ * Event Y coordinate relative to the map
+ * @public
+ * @type {Number}
+ * @name gameWorldY
+ * @memberOf me.Pointer
+ */
+
+ this.gameWorldY = 0;
+ /**
+ * Event X coordinate relative to the holding container
+ * @public
+ * @type {Number}
+ * @name gameLocalX
+ * @memberOf me.Pointer
+ */
+
+ this.gameLocalX = 0;
+ /**
+ * Event Y coordinate relative to the holding container
+ * @public
+ * @type {Number}
+ * @name gameLocalY
+ * @memberOf me.Pointer
+ */
+
+ this.gameLocalY = 0;
+ /**
+ * The unique identifier of the contact for a touch, mouse or pen
+ * @public
+ * @type {Number}
+ * @name pointerId
+ * @memberOf me.Pointer
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/pointerId
+ */
+
+ this.pointerId = undefined; // parent constructor
+
+ this._super(me.Rect, "init", [x || 0, y || 0, w || 1, h || 1]);
+ },
+
+ /**
+ * initialize the Pointer object using the given Event Object
+ * @name me.Pointer#set
+ * @private
+ * @function
+ * @param {Event} event the original Event object
+ * @param {Number} pageX the horizontal coordinate at which the event occurred, relative to the left edge of the entire document
+ * @param {Number} pageY the vertical coordinate at which the event occurred, relative to the left edge of the entire document
+ * @param {Number} clientX the horizontal coordinate within the application's client area at which the event occurred
+ * @param {Number} clientX the vertical coordinate within the application's client area at which the event occurred
+ * @param {Number} pointedId the Pointer, Touch or Mouse event Id
+ */
+ setEvent: function setEvent(event, pageX, pageY, clientX, clientY, pointerId) {
+ var width = 1;
+ var height = 1; // the original event object
+
+ this.event = event;
+ this.pageX = pageX || 0;
+ this.pageY = pageY || 0;
+ this.clientX = clientX || 0;
+ this.clientY = clientY || 0; // translate to local coordinates
+
+ me.input.globalToLocal(this.pageX, this.pageY, this.pos); // true if not originally a pointer event
+
+ this.isNormalized = !me.device.PointerEvent || me.device.PointerEvent && !(event instanceof window.PointerEvent);
+
+ if (event.type === "wheel") {
+ this.deltaMode = 1;
+ this.deltaX = event.deltaX;
+ this.deltaY = -1 / 40 * event.wheelDelta;
+ event.wheelDeltaX && (this.deltaX = -1 / 40 * event.wheelDeltaX);
+ } // could be 0, so test if defined
+
+
+ this.pointerId = typeof pointerId !== "undefined" ? pointerId : 1;
+ this.isPrimary = typeof event.isPrimary !== "undefined" ? event.isPrimary : true; // in case of touch events, button is not defined
+
+ this.button = event.button || 0;
+ this.type = event.type;
+ this.gameScreenX = this.pos.x;
+ this.gameScreenY = this.pos.y; // get the current screen to world offset
+
+ if (typeof me.game.viewport !== "undefined") {
+ me.game.viewport.localToWorld(this.gameScreenX, this.gameScreenY, viewportOffset);
+ }
+ /* Initialize the two coordinate space properties. */
+
+
+ this.gameWorldX = viewportOffset.x;
+ this.gameWorldY = viewportOffset.y; // get the pointer size
+
+ if (this.isNormalized === false) {
+ // native PointerEvent
+ width = event.width || 1;
+ height = event.height || 1;
+ } else if (typeof event.radiusX === "number") {
+ // TouchEvent
+ width = event.radiusX * 2 || 1;
+ height = event.radiusY * 2 || 1;
+ } // resize the pointer object accordingly
+
+
+ this.resize(width, height);
+ }
+ });
+ })();
+
+ (function (api) {
+ /**
+ * A pool of `Pointer` objects to cache pointer/touch event coordinates.
+ * @type {Array.
+ * properties :
+ * LEFT : constant for left button
+ * MIDDLE : constant for middle button
+ * RIGHT : constant for right button
+ * @public
+ * @type {me.Rect}
+ * @name pointer
+ * @memberOf me.input
+ */
+
+
+ api.pointer = new me.Pointer(0, 0, 1, 1); // bind list for mouse buttons
+
+ api.pointer.bind = [0, 0, 0]; // W3C button constants
+
+ api.pointer.LEFT = 0;
+ api.pointer.MIDDLE = 1;
+ api.pointer.RIGHT = 2;
+ /**
+ * time interval for event throttling in milliseconds
+ * default value : "1000/me.sys.fps" ms
+ * set to 0 ms to disable the feature
+ * @public
+ * @type Number
+ * @name throttlingInterval
+ * @memberOf me.input
+ */
+
+ api.throttlingInterval = undefined;
+ /**
+ * Translate the specified x and y values from the global (absolute)
+ * coordinate to local (viewport) relative coordinate.
+ * @name globalToLocal
+ * @memberOf me.input
+ * @public
+ * @function
+ * @param {Number} x the global x coordinate to be translated.
+ * @param {Number} y the global y coordinate to be translated.
+ * @param {Number} [v] an optional vector object where to set the
+ * @return {me.Vector2d} A vector object with the corresponding translated coordinates.
+ * @example
+ * onMouseEvent : function (pointer) {
+ * // convert the given into local (viewport) relative coordinates
+ * var pos = me.input.globalToLocal(pointer.clientX, pointer.clientY);
+ * // do something with pos !
+ * };
+ */
+
+ api.globalToLocal = function (x, y, v) {
+ v = v || new me.Vector2d();
+ var parent = me.video.renderer.getBounds();
+ var pixelRatio = me.device.devicePixelRatio;
+ x -= parent.left;
+ y -= parent.top;
+ var scale = me.sys.scale;
+
+ if (scale.x !== 1.0 || scale.y !== 1.0) {
+ x /= scale.x;
+ y /= scale.y;
+ }
+
+ return v.set(x * pixelRatio, y * pixelRatio);
+ };
+ /**
+ * enable/disable all gestures on the given element.
+ * by default melonJS will disable browser handling of all panning and zooming gestures.
+ * @name setTouchAction
+ * @memberOf me.input
+ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action
+ * @public
+ * @function
+ * @param {HTMLCanvasElement} element
+ * @param {String} [value="none"]
+ */
+
+
+ api.setTouchAction = function (element, value) {
+ element.style["touch-action"] = value || "none";
+ };
+ /**
+ * Associate a pointer event to a keycode
+ * Left button – 0
+ * Middle button – 1
+ * Right button – 2
+ * @name bindPointer
+ * @memberOf me.input
+ * @public
+ * @function
+ * @param {Number} [button=me.input.pointer.LEFT] (accordingly to W3C values : 0,1,2 for left, middle and right buttons)
+ * @param {me.input.KEY} keyCode
+ * @example
+ * // enable the keyboard
+ * me.input.bindKey(me.input.KEY.X, "shoot");
+ * // map the left button click on the X key (default if the button is not specified)
+ * me.input.bindPointer(me.input.KEY.X);
+ * // map the right button click on the X key
+ * me.input.bindPointer(me.input.pointer.RIGHT, me.input.KEY.X);
+ */
+
+
+ api.bindPointer = function () {
+ var button = arguments.length < 2 ? api.pointer.LEFT : arguments[0];
+ var keyCode = arguments.length < 2 ? arguments[0] : arguments[1]; // make sure the mouse is initialized
+
+ enablePointerEvent(); // throw an exception if no action is defined for the specified keycode
+
+ if (!api._KeyBinding[keyCode]) {
+ throw new Error("no action defined for keycode " + keyCode);
+ } // map the mouse button to the keycode
+
+
+ api.pointer.bind[button] = keyCode;
+ };
+ /**
+ * unbind the defined keycode
+ * @name unbindPointer
+ * @memberOf me.input
+ * @public
+ * @function
+ * @param {Number} [button=me.input.pointer.LEFT] (accordingly to W3C values : 0,1,2 for left, middle and right buttons)
+ * @example
+ * me.input.unbindPointer(me.input.pointer.LEFT);
+ */
+
+
+ api.unbindPointer = function (button) {
+ // clear the event status
+ api.pointer.bind[typeof button === "undefined" ? api.pointer.LEFT : button] = null;
+ };
+ /**
+ * allows registration of event listeners on the object target.
+ * melonJS will pass a me.Pointer object to the defined callback.
+ * @see me.Pointer
+ * @see {@link http://www.w3.org/TR/pointerevents/#list-of-pointer-events|W3C Pointer Event list}
+ * @name registerPointerEvent
+ * @memberOf me.input
+ * @public
+ * @function
+ * @param {String} eventType The event type for which the object is registering
+ * melonJS currently supports:
+ *
+ *
+ * @param {me.Rect|me.Polygon|me.Line|me.Ellipse} region a shape representing the region to register on
+ * @param {Function} callback methods to be called when the event occurs.
+ * Returning `false` from the defined callback will prevent the event to be propagated to other objects
+ * @example
+ * // onActivate function
+ * onActivateEvent: function () {
+ * // register on the 'pointerdown' event
+ * me.input.registerPointerEvent('pointerdown', this, this.pointerDown.bind(this));
+ * },
+ *
+ * // pointerDown event callback
+ * pointerDown: function (pointer) {
+ * // do something
+ * ....
+ * // don"t propagate the event to other objects
+ * return false;
+ * },
+ */
+
+
+ api.registerPointerEvent = function (eventType, region, callback) {
+ // make sure the mouse/touch events are initialized
+ enablePointerEvent();
+
+ if (pointerEventList.indexOf(eventType) === -1) {
+ throw new Error("invalid event type : " + eventType);
+ }
+
+ if (typeof region === "undefined") {
+ throw new Error("registerPointerEvent: region for " + region + " event is undefined ");
+ }
+
+ var eventTypes = findAllActiveEvents(activeEventList, pointerEventMap[eventType]); // register the event
+
+ if (!eventHandlers.has(region)) {
+ eventHandlers.set(region, {
+ region: region,
+ callbacks: {},
+ pointerId: null
+ });
+ } // allocate array if not defined
+
+
+ var handlers = eventHandlers.get(region);
+
+ for (var i = 0; i < eventTypes.length; i++) {
+ eventType = eventTypes[i];
+
+ if (handlers.callbacks[eventType]) {
+ handlers.callbacks[eventType].push(callback);
+ } else {
+ handlers.callbacks[eventType] = [callback];
+ }
+ }
+ };
+ /**
+ * allows the removal of event listeners from the object target.
+ * @see {@link http://www.w3.org/TR/pointerevents/#list-of-pointer-events|W3C Pointer Event list}
+ * @name releasePointerEvent
+ * @memberOf me.input
+ * @public
+ * @function
+ * @param {String} eventType The event type for which the object was registered. See {@link me.input.registerPointerEvent}
+ * @param {me.Rect|me.Polygon|me.Line|me.Ellipse} region the registered region to release for this event
+ * @param {Function} [callback="all"] if specified unregister the event only for the specific callback
+ * @example
+ * // release the registered region on the 'pointerdown' event
+ * me.input.releasePointerEvent('pointerdown', this);
+ */
+
+
+ api.releasePointerEvent = function (eventType, region, callback) {
+ if (pointerEventList.indexOf(eventType) === -1) {
+ throw new Error("invalid event type : " + eventType);
+ } // convert to supported event type if pointerEvent not natively supported
+
+
+ var eventTypes = findAllActiveEvents(activeEventList, pointerEventMap[eventType]);
+ var handlers = eventHandlers.get(region);
+
+ if (typeof handlers !== "undefined") {
+ for (var i = 0; i < eventTypes.length; i++) {
+ eventType = eventTypes[i];
+
+ if (handlers.callbacks[eventType]) {
+ if (typeof callback !== "undefined") {
+ me.utils.array.remove(handlers.callbacks[eventType], callback);
+ } else {
+ while (handlers.callbacks[eventType].length > 0) {
+ handlers.callbacks[eventType].pop();
+ }
+ } // free the array if empty
+
+
+ if (handlers.callbacks[eventType].length === 0) {
+ delete handlers.callbacks[eventType];
+ }
+ }
+ }
+
+ if (Object.keys(handlers.callbacks).length === 0) {
+ eventHandlers.delete(region);
+ }
+ }
+ };
+ })(me.input);
+
+ (function (api) {
+ /*
+ * PRIVATE STUFF
+ */
+ // Analog deadzone
+ var deadzone = 0.1;
+ /**
+ * A function that returns a normalized value in range [-1.0..1.0], or 0.0 if the axis is unknown.
+ * @callback me.input~normalize_fn
+ * @param {Number} value The raw value read from the gamepad driver
+ * @param {Number} axis The axis index from the standard mapping, or -1 if not an axis
+ * @param {Number} button The button index from the standard mapping, or -1 if not a button
+ */
+
+ function defaultNormalizeFn(value) {
+ return value;
+ }
+ /**
+ * Normalize axis values for wired Xbox 360
+ * @ignore
+ */
+
+
+ function wiredXbox360NormalizeFn(value, axis, button) {
+ if (button === api.GAMEPAD.BUTTONS.L2 || button === api.GAMEPAD.BUTTONS.R2) {
+ return (value + 1) / 2;
+ }
+
+ return value;
+ }
+ /**
+ * Normalize axis values for OUYA
+ * @ignore
+ */
+
+
+ function ouyaNormalizeFn(value, axis, button) {
+ if (value > 0) {
+ if (button === api.GAMEPAD.BUTTONS.L2) {
+ // L2 is wonky; seems like the deadzone is around 20000
+ // (That's over 15% of the total range!)
+ value = Math.max(0, value - 20000) / 111070;
+ } else {
+ // Normalize [1..65536] => [0.0..0.5]
+ value = (value - 1) / 131070;
+ }
+ } else {
+ // Normalize [-65536..-1] => [0.5..1.0]
+ value = (65536 + value) / 131070 + 0.5;
+ }
+
+ return value;
+ } // Match vendor and product codes for Firefox
+
+
+ var vendorProductRE = /^([0-9a-f]{1,4})-([0-9a-f]{1,4})-/i; // Match leading zeros
+
+ var leadingZeroRE = /^0+/;
+ /**
+ * Firefox reports different ids for gamepads depending on the platform:
+ * - Windows: vendor and product codes contain leading zeroes
+ * - Mac: vendor and product codes are sparse (no leading zeroes)
+ *
+ * This function normalizes the id to support both formats
+ * @ignore
+ */
+
+ function addMapping(id, mapping) {
+ var expanded_id = id.replace(vendorProductRE, function (_, a, b) {
+ return "000".substr(a.length - 1) + a + "-" + "000".substr(b.length - 1) + b + "-";
+ });
+ var sparse_id = id.replace(vendorProductRE, function (_, a, b) {
+ return a.replace(leadingZeroRE, "") + "-" + b.replace(leadingZeroRE, "") + "-";
+ }); // Normalize optional parameters
+
+ mapping.analog = mapping.analog || mapping.buttons.map(function () {
+ return -1;
+ });
+ mapping.normalize_fn = mapping.normalize_fn || defaultNormalizeFn;
+ remap.set(expanded_id, mapping);
+ remap.set(sparse_id, mapping);
+ } // binding list
+
+
+ var bindings = {}; // mapping list
+
+ var remap = new Map();
+ /**
+ * Default gamepad mappings
+ * @ignore
+ */
+
+ [// Firefox mappings
+ ["45e-28e-Xbox 360 Wired Controller", {
+ "axes": [0, 1, 3, 4],
+ "buttons": [11, 12, 13, 14, 8, 9, -1, -1, 5, 4, 6, 7, 0, 1, 2, 3, 10],
+ "analog": [-1, -1, -1, -1, -1, -1, 2, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1],
+ "normalize_fn": wiredXbox360NormalizeFn
+ }], ["54c-268-PLAYSTATION(R)3 Controller", {
+ "axes": [0, 1, 2, 3],
+ "buttons": [14, 13, 15, 12, 10, 11, 8, 9, 0, 3, 1, 2, 4, 6, 7, 5, 16]
+ }], ["54c-5c4-Wireless Controller", // PS4 Controller
+ {
+ "axes": [0, 1, 2, 3],
+ "buttons": [1, 0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 14, 15, 16, 17, 12, 13]
+ }], ["2836-1-OUYA Game Controller", {
+ "axes": [0, 3, 7, 9],
+ "buttons": [3, 6, 4, 5, 7, 8, 15, 16, -1, -1, 9, 10, 11, 12, 13, 14, -1],
+ "analog": [-1, -1, -1, -1, -1, -1, 5, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1],
+ "normalize_fn": ouyaNormalizeFn
+ }], // Chrome mappings
+ ["OUYA Game Controller (Vendor: 2836 Product: 0001)", {
+ "axes": [0, 1, 3, 4],
+ "buttons": [0, 3, 1, 2, 4, 5, 12, 13, -1, -1, 6, 7, 8, 9, 10, 11, -1],
+ "analog": [-1, -1, -1, -1, -1, -1, 2, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1],
+ "normalize_fn": ouyaNormalizeFn
+ }]].forEach(function (value) {
+ addMapping(value[0], value[1]);
+ });
+ /**
+ * gamepad connected callback
+ * @ignore
+ */
+
+ window.addEventListener("gamepadconnected", function (event) {
+ me.event.publish(me.event.GAMEPAD_CONNECTED, [event.gamepad]);
+ }, false);
+ /**
+ * gamepad disconnected callback
+ * @ignore
+ */
+
+ window.addEventListener("gamepaddisconnected", function (event) {
+ me.event.publish(me.event.GAMEPAD_DISCONNECTED, [event.gamepad]);
+ }, false);
+ /**
+ * Update gamepad status
+ * @ignore
+ */
+
+ api._updateGamepads = navigator.getGamepads ? function () {
+ var gamepads = navigator.getGamepads();
+ var e = {}; // Trigger button bindings
+
+ Object.keys(bindings).forEach(function (index) {
+ var gamepad = gamepads[index];
+
+ if (!gamepad) {
+ return;
+ }
+
+ var mapping = null;
+
+ if (gamepad.mapping !== "standard") {
+ mapping = remap.get(gamepad.id);
+ }
+
+ var binding = bindings[index]; // Iterate all buttons that have active bindings
+
+ Object.keys(binding.buttons).forEach(function (button) {
+ var last = binding.buttons[button];
+ var mapped_button = button;
+ var mapped_axis = -1; // Remap buttons if necessary
+
+ if (mapping) {
+ mapped_button = mapping.buttons[button];
+ mapped_axis = mapping.analog[button];
+
+ if (mapped_button < 0 && mapped_axis < 0) {
+ // Button is not mapped
+ return;
+ }
+ } // Get mapped button
+
+
+ var current = gamepad.buttons[mapped_button] || {}; // Remap an axis to an analog button
+
+ if (mapping) {
+ if (mapped_axis >= 0) {
+ var value = mapping.normalize_fn(gamepad.axes[mapped_axis], -1, +button); // Create a new object, because GamepadButton is read-only
+
+ current = {
+ "value": value,
+ "pressed": current.pressed || Math.abs(value) >= deadzone
+ };
+ }
+ }
+
+ me.event.publish(me.event.GAMEPAD_UPDATE, [index, "buttons", +button, current]); // Edge detection
+
+ if (!last.pressed && current.pressed) {
+ api._keydown(e, last.keyCode, mapped_button + 256);
+ } else if (last.pressed && !current.pressed) {
+ api._keyup(e, last.keyCode, mapped_button + 256);
+ } // Update last button state
+
+
+ last.value = current.value;
+ last.pressed = current.pressed;
+ }); // Iterate all axes that have active bindings
+
+ Object.keys(binding.axes).forEach(function (axis) {
+ var last = binding.axes[axis];
+ var mapped_axis = axis; // Remap buttons if necessary
+
+ if (mapping) {
+ mapped_axis = mapping.axes[axis];
+
+ if (mapped_axis < 0) {
+ // axe is not mapped
+ return;
+ }
+ } // retrieve the current value and normalize if necessary
+
+
+ var value = gamepad.axes[mapped_axis];
+
+ if (typeof value === "undefined") {
+ return;
+ }
+
+ if (mapping) {
+ value = mapping.normalize_fn(value, +axis, -1);
+ } // normalize value into a [-1, 1] range value (treat 0 as positive)
+
+
+ var range = Math.sign(value) || 1;
+
+ if (last[range].keyCode === 0) {
+ return;
+ }
+
+ var pressed = Math.abs(value) >= deadzone + Math.abs(last[range].threshold);
+ me.event.publish(me.event.GAMEPAD_UPDATE, [index, "axes", +axis, value]); // Edge detection
+
+ if (!last[range].pressed && pressed) {
+ // Release the opposite direction, if necessary
+ if (last[-range].pressed) {
+ api._keyup(e, last[-range].keyCode, mapped_axis + 256);
+
+ last[-range].value = 0;
+ last[-range].pressed = false;
+ }
+
+ api._keydown(e, last[range].keyCode, mapped_axis + 256);
+ } else if ((last[range].pressed || last[-range].pressed) && !pressed) {
+ range = last[range].pressed ? range : -range;
+
+ api._keyup(e, last[range].keyCode, mapped_axis + 256);
+ } // Update last axis state
+
+
+ last[range].value = value;
+ last[range].pressed = pressed;
+ });
+ });
+ } : function () {};
+ /*
+ * PUBLIC STUFF
+ */
+
+ /**
+ * Namespace for standard gamepad mapping constants
+ * @public
+ * @namespace GAMEPAD
+ * @memberOf me.input
+ */
+
+ api.GAMEPAD = {
+ /**
+ * Standard gamepad mapping information for axes"pointermove"
"pointerdown"
"pointerup"
"pointerenter"
"pointerover"
"pointerleave"
"pointercancel"
"wheel"
+ *
+ *
+ * @public
+ * @name AXES
+ * @enum {Number}
+ * @memberOf me.input.GAMEPAD
+ * @see https://w3c.github.io/gamepad/#remapping
+ */
+ "AXES": {
+ "LX": 0,
+ "LY": 1,
+ "RX": 2,
+ "RY": 3,
+ "EXTRA_1": 4,
+ "EXTRA_2": 5,
+ "EXTRA_3": 6,
+ "EXTRA_4": 7
+ },
+
+ /**
+ * Standard gamepad mapping information for buttonsLX
(horizontal), LY
(vertical)RX
(horizontal), RY
(vertical)EXTRA_1
, EXTRA_2
, EXTRA_3
, EXTRA_4
+ *
+ *
+ * @public
+ * @name BUTTONS
+ * @enum {Number}
+ * @memberOf me.input.GAMEPAD
+ * @see https://w3c.github.io/gamepad/#remapping
+ */
+ "BUTTONS": {
+ "FACE_1": 0,
+ "FACE_2": 1,
+ "FACE_3": 2,
+ "FACE_4": 3,
+ "L1": 4,
+ "R1": 5,
+ "L2": 6,
+ "R2": 7,
+ "SELECT": 8,
+ "BACK": 8,
+ "START": 9,
+ "FORWARD": 9,
+ "L3": 10,
+ "R3": 11,
+ "UP": 12,
+ "DOWN": 13,
+ "LEFT": 14,
+ "RIGHT": 15,
+ "HOME": 16,
+ "EXTRA_1": 17,
+ "EXTRA_2": 18,
+ "EXTRA_3": 19,
+ "EXTRA_4": 20
+ }
+ };
+ /**
+ * Associate a gamepad event to a keycode
+ * @name bindGamepad
+ * @memberOf me.input
+ * @public
+ * @function
+ * @param {Number} index Gamepad index
+ * @param {Object} button Button/Axis definition
+ * @param {String} button.type "buttons" or "axes"
+ * @param {me.input.GAMEPAD.BUTTONS|me.input.GAMEPAD.AXES} button.code button or axis code id
+ * @param {Number} [button.threshold=1] value indicating when the axis should trigger the keycode (e.g. -0.5 or 0.5)
+ * @param {me.input.KEY} keyCode
+ * @example
+ * // enable the keyboard
+ * me.input.bindKey(me.input.KEY.X, "shoot");
+ * ...
+ * // map the lower face button on the first gamepad to the X key
+ * me.input.bindGamepad(0, {type:"buttons", code: me.input.GAMEPAD.BUTTONS.FACE_1}, me.input.KEY.X);
+ * // map the left axis value on the first gamepad to the LEFT key
+ * me.input.bindGamepad(0, {type:"axes", code: me.input.GAMEPAD.AXES.LX, threshold: -0.5}, me.input.KEY.LEFT);
+ */
+
+ api.bindGamepad = function (index, button, keyCode) {
+ // Throw an exception if no action is defined for the specified keycode
+ if (!api._KeyBinding[keyCode]) {
+ throw new Error("no action defined for keycode " + keyCode);
+ } // Allocate bindings if not defined
+
+
+ if (!bindings[index]) {
+ bindings[index] = {
+ "axes": {},
+ "buttons": {}
+ };
+ }
+
+ var mapping = {
+ "keyCode": keyCode,
+ "value": 0,
+ "pressed": false,
+ "threshold": button.threshold // can be undefined
+
+ };
+ var binding = bindings[index][button.type]; // Map the gamepad button or axis to the keycode
+
+ if (button.type === "buttons") {
+ // buttons are defined by a `gamePadButton` object
+ binding[button.code] = mapping;
+ } else if (button.type === "axes") {
+ // normalize threshold into a value that can represent both side of the axis
+ var range = Math.sign(button.threshold) || 1; // axes are defined using two objects; one for negative and one for positive
+
+ if (!binding[button.code]) {
+ binding[button.code] = {};
+ }
+
+ var axes = binding[button.code];
+ axes[range] = mapping; // Ensure the opposite axis exists
+
+ if (!axes[-range]) {
+ axes[-range] = {
+ "keyCode": 0,
+ "value": 0,
+ "pressed": false,
+ "threshold": -range
+ };
+ }
+ }
+ };
+ /**
+ * unbind the defined keycode
+ * @name unbindGamepad
+ * @memberOf me.input
+ * @public
+ * @function
+ * @param {Number} index Gamepad index
+ * @param {me.input.GAMEPAD.BUTTONS} button
+ * @example
+ * me.input.unbindGamepad(0, me.input.GAMEPAD.BUTTONS.FACE_1);
+ */
+
+
+ api.unbindGamepad = function (index, button) {
+ if (!bindings[index]) {
+ throw new Error("no bindings for gamepad " + index);
+ }
+
+ bindings[index].buttons[button] = {};
+ };
+ /**
+ * Set deadzone for analog gamepad inputsFACE_1
, FACE_2
, FACE_3
, FACE_4
UP
, DOWN
, LEFT
, RIGHT
L1
, L2
, R1
, R2
L3
, R3
SELECT
(BACK
), START
(FORWARD
), HOME
EXTRA_1
, EXTRA_2
, EXTRA_3
, EXTRA_4
+ * The default deadzone is 0.1 (10%) Analog values less than this will be ignored
+ * @name setGamepadDeadzone
+ * @memberOf me.input
+ * @public
+ * @function
+ * @param {Number} value Deadzone value
+ */
+
+
+ api.setGamepadDeadzone = function (value) {
+ deadzone = value;
+ };
+ /**
+ * specify a custom mapping for a specific gamepad id
+ * see below for the default mapping :
+ *
+ * @name setGamepadMapping
+ * @memberOf me.input
+ * @public
+ * @function
+ * @param {String} id Gamepad id string
+ * @param {Object} mapping A hash table
+ * @param {Number[]} mapping.axes Standard analog control stick axis locations
+ * @param {Number[]} mapping.buttons Standard digital button locations
+ * @param {Number[]} [mapping.analog] Analog axis locations for buttons
+ * @param {me.input~normalize_fn} [mapping.normalize_fn] Axis normalization function
+ * @example
+ * // A weird controller that has its axis mappings reversed
+ * me.input.setGamepadMapping("Generic USB Controller", {
+ * "axes" : [ 3, 2, 1, 0 ],
+ * "buttons" : [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 ]
+ * });
+ *
+ * // Mapping extra axes to analog buttons
+ * me.input.setGamepadMapping("Generic Analog Controller", {
+ * "axes" : [ 0, 1, 2, 3 ],
+ * "buttons" : [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 ],
+ *
+ * // Raw axis 4 is mapped to GAMEPAD.BUTTONS.FACE_1
+ * // Raw axis 5 is mapped to GAMEPAD.BUTTONS.FACE_2
+ * // etc...
+ * // Also maps left and right triggers
+ * "analog" : [ 4, 5, 6, 7, -1, -1, 8, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1 ],
+ *
+ * // Normalize the value of button L2: [-1.0..1.0] => [0.0..1.0]
+ * "normalize_fn" : function (value, axis, button) {
+ * return ((button === me.input.GAMEPAD.BUTTONS.L2) ? ((value + 1) / 2) : value) || 0;
+ * }
+ * });
+ */
+
+
+ api.setGamepadMapping = addMapping;
+ })(me.input);
+
+ (function () {
+ /**
+ * a collection of utility functions
+ * @namespace me.utils
+ * @memberOf me
+ */
+ me.utils = function () {
+ // hold public stuff in our singleton
+ var api = {};
+ /*
+ * PRIVATE STUFF
+ */
+ // guid default value
+
+ var GUID_base = "";
+ var GUID_index = 0;
+ /*
+ * PUBLIC STUFF
+ */
+
+ /**
+ * Get image pixels
+ * @public
+ * @function
+ * @memberOf me.utils
+ * @name getPixels
+ * @param {Image|Canvas} image Image to read
+ * @return {ImageData} Canvas ImageData object
+ */
+
+ api.getPixels = function (arg) {
+ if (arg instanceof HTMLImageElement) {
+ var _context = me.CanvasRenderer.getContext2d(me.video.createCanvas(arg.width, arg.height));
+
+ _context.drawImage(arg, 0, 0);
+
+ return _context.getImageData(0, 0, arg.width, arg.height);
+ } else {
+ // canvas !
+ return arg.getContext("2d").getImageData(0, 0, arg.width, arg.height);
+ }
+ };
+ /**
+ * reset the GUID Base Name
+ * the idea here being to have a unique ID
+ * per level / object
+ * @ignore
+ */
+
+
+ api.resetGUID = function (base, index) {
+ // also ensure it's only 8bit ASCII characters
+ GUID_base = me.utils.string.toHex(base.toString().toUpperCase());
+ GUID_index = index || 0;
+ };
+ /**
+ * create and return a very simple GUID
+ * Game Unique ID
+ * @ignore
+ */
+
+
+ api.createGUID = function (index) {
+ // to cover the case of undefined id for groups
+ GUID_index += index || 1;
+ return GUID_base + "-" + (index || GUID_index);
+ }; // return our object
+
+
+ return api;
+ }();
+ })();
+
+ (function (api) {
+ /**
+ * a collection of file utility functions
+ * @namespace me.utils.file
+ * @memberOf me
+ */
+ var file = function () {
+ // hold public stuff in our singleton
+ var api = {}; // regexp to deal with file name & path
+
+ var REMOVE_PATH = /^.*(\\|\/|\:)/;
+ var REMOVE_EXT = /\.[^\.]*$/;
+ /**
+ * return the base name of the file without path info
+ * @public
+ * @function
+ * @memberOf me.utils.file
+ * @name getBasename
+ * @param {String} path path containing the filename
+ * @return {String} the base name without path information.
+ */
+
+ api.getBasename = function (path) {
+ return path.replace(REMOVE_PATH, "").replace(REMOVE_EXT, "");
+ };
+ /**
+ * return the extension of the file in the given path
+ * @public
+ * @function
+ * @memberOf me.utils.file
+ * @name getExtension
+ * @param {String} path path containing the filename
+ * @return {String} filename extension.
+ */
+
+
+ api.getExtension = function (path) {
+ return path.substring(path.lastIndexOf(".") + 1, path.length);
+ }; // return our object
+
+
+ return api;
+ }();
+
+ api.file = file;
+ })(me.utils);
+
+ (function (api) {
+ /**
+ * a collection of utility functions
+ * @namespace me.utils.function
+ * @memberOf me
+ */
+ var fn = function () {
+ // hold public stuff in our singleton
+ var api = {};
+ /**
+ * Executes a function as soon as the interpreter is idle (stack empty).
+ * @public
+ * @function
+ * @memberOf me.utils.function
+ * @name defer
+ * @param {Function} fn The function to be deferred.
+ * @param {Object} scope The execution scope of the deferred function.
+ * @param {} [arguments...] Optional additional arguments to carry for the
+ * function.
+ * @return {Number} id that can be used to clear the deferred function using
+ * clearTimeout
+ * @example
+ * // execute myFunc() when the stack is empty,
+ * // with the current context and 'myArgument' as parameter
+ * me.utils.function.defer(fn, this, 'myArgument');
+ */
+
+ api.defer = function (fn, scope) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ return setTimeout(fn.bind.apply(fn, args), 0.01);
+ };
+ /**
+ * returns a function that, when invoked will only be triggered at most
+ * once during a given window of time
+ * @public
+ * @function
+ * @memberOf me.utils.function
+ * @name throttle
+ * @param {Function} fn the function to be throttled.
+ * @param {Number} delay The delay in ms
+ * @param {no_trailing} no_trailing disable the execution on the trailing edge
+ */
+
+
+ api.throttle = function (fn, delay, no_trailing) {
+ var last = window.performance.now(),
+ deferTimer; // `no_trailing` defaults to false.
+
+ if (typeof no_trailing !== "boolean") {
+ no_trailing = false;
+ }
+
+ return function () {
+ var now = window.performance.now();
+ var elasped = now - last;
+ var args = arguments;
+
+ if (elasped < delay) {
+ if (no_trailing === false) {
+ // hold on to it
+ clearTimeout(deferTimer);
+ deferTimer = setTimeout(function () {
+ last = now;
+ return fn.apply(null, args);
+ }, elasped);
+ }
+ } else {
+ last = now;
+ return fn.apply(null, args);
+ }
+ };
+ }; // return our object
+
+
+ return api;
+ }();
+
+ api.function = fn;
+ })(me.utils);
+
+ (function (api) {
+ /**
+ * a collection of array utility functions
+ * @namespace me.utils.array
+ * @memberOf me
+ */
+ var array = function () {
+ // hold public stuff in our singleton
+ var api = {};
+ /**
+ * Remove the specified object from the given Array
+ * @public
+ * @function
+ * @memberOf me.utils.array
+ * @name remove
+ * @param {Array} arr array from which to remove an object
+ * @param {Object} object to be removed
+ * @return {Array} the modified Array
+ */
+
+ api.remove = function (arr, obj) {
+ var i = Array.prototype.indexOf.call(arr, obj);
+
+ if (i !== -1) {
+ Array.prototype.splice.call(arr, i, 1);
+ }
+
+ return arr;
+ };
+ /**
+ * return a random array element
+ * @public
+ * @function
+ * @memberOf me.utils.array
+ * @name random
+ * @param {Array} arr array to pick a element
+ * @return {any} random member of array
+ * @example
+ * // Select a random array element
+ * var arr = [ "foo", "bar", "baz" ];
+ * console.log(me.utils.array.random(arr));
+ */
+
+
+ api.random = function (arr) {
+ return arr[me.Math.random(0, arr.length)];
+ };
+ /**
+ * return a weighted random array element, favoring the earlier entries
+ * @public
+ * @function
+ * @memberOf me.utils.array
+ * @name weightedRandom
+ * @param {Array} arr array to pick a element
+ * @return {any} random member of array
+ */
+
+
+ api.weightedRandom = function (arr) {
+ return arr[me.Math.weightedRandom(0, arr.length)];
+ }; // return our object
+
+
+ return api;
+ }();
+
+ api.array = array;
+ })(me.utils);
+
+ (function (api) {
+ /**
+ * a collection of string utility functions
+ * @namespace me.utils.string
+ * @memberOf me
+ */
+ var string = function () {
+ // hold public stuff in our singleton
+ var api = {};
+ /**
+ * returns the string stripped of whitespace from the left.
+ * @public
+ * @function
+ * @memberOf me.utils.string
+ * @name trimLeft
+ * @param {String} string the string to be trimmed
+ * @return {string} trimmed string
+ */
+
+ api.trimLeft = function (str) {
+ return str.replace(/^\s+/, "");
+ };
+ /**
+ * returns the string stripped of whitespace from the right.
+ * @public
+ * @function
+ * @memberOf me.utils.string
+ * @name trimRight
+ * @param {String} string the string to be trimmed
+ * @return {string} trimmed string
+ */
+
+
+ api.trimRight = function (str) {
+ return str.replace(/\s+$/, "");
+ };
+ /**
+ * returns true if the given string contains a numeric value
+ * @public
+ * @function
+ * @memberOf me.utils.string
+ * @name isNumeric
+ * @param {String} string the string to be tested
+ * @return {Boolean} true if string contains only digits
+ */
+
+
+ api.isNumeric = function (str) {
+ return !isNaN(str) && str.trim() !== "";
+ };
+ /**
+ * returns true if the given string contains a true or false
+ * @public
+ * @function
+ * @memberOf me.utils.string
+ * @name isBoolean
+ * @param {String} string the string to be tested
+ * @return {Boolean} true if the string is either true or false
+ */
+
+
+ api.isBoolean = function (str) {
+ var trimmed = str.trim();
+ return trimmed === "true" || trimmed === "false";
+ };
+ /**
+ * convert a string to the corresponding hexadecimal value
+ * @public
+ * @function
+ * @memberOf me.utils.string
+ * @name toHex
+ * @param {String} string the string to be converted
+ * @return {String}
+ */
+
+
+ api.toHex = function (str) {
+ var res = "",
+ c = 0;
+
+ while (c < str.length) {
+ res += str.charCodeAt(c++).toString(16);
+ }
+
+ return res;
+ }; // return our object
+
+
+ return api;
+ }();
+
+ api.string = string;
+ })(me.utils);
+
+ (function () {
+ // convert a give color component to it hexadecimal value
+ var _toHex = function toHex(component) {
+ return "0123456789ABCDEF".charAt(component - component % 16 >> 4) + "0123456789ABCDEF".charAt(component % 16);
+ };
+
+ var rgbaRx = /^rgba?\((\d+), ?(\d+), ?(\d+)(, ?([\d\.]+))?\)$/;
+ var hex3Rx = /^#([\da-fA-F])([\da-fA-F])([\da-fA-F])$/;
+ var hex4Rx = /^#([\da-fA-F])([\da-fA-F])([\da-fA-F])([\da-fA-F])$/;
+ var hex6Rx = /^#([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})$/;
+ var hex8Rx = /^#([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})$/;
+ var cssToRGB = new Map();
+ [// CSS1
+ ["black", [0, 0, 0]], ["silver", [192, 192, 129]], ["gray", [128, 128, 128]], ["white", [255, 255, 255]], ["maroon", [128, 0, 0]], ["red", [255, 0, 0]], ["purple", [128, 0, 128]], ["fuchsia", [255, 0, 255]], ["green", [0, 128, 0]], ["lime", [0, 255, 0]], ["olive", [128, 128, 0]], ["yellow", [255, 255, 0]], ["navy", [0, 0, 128]], ["blue", [0, 0, 255]], ["teal", [0, 128, 128]], ["aqua", [0, 255, 255]], // CSS2
+ ["orange", [255, 165, 0]], // CSS3
+ ["aliceblue", [240, 248, 245]], ["antiquewhite", [250, 235, 215]], ["aquamarine", [127, 255, 212]], ["azure", [240, 255, 255]], ["beige", [245, 245, 220]], ["bisque", [255, 228, 196]], ["blanchedalmond", [255, 235, 205]], ["blueviolet", [138, 43, 226]], ["brown", [165, 42, 42]], ["burlywood", [222, 184, 35]], ["cadetblue", [95, 158, 160]], ["chartreuse", [127, 255, 0]], ["chocolate", [210, 105, 30]], ["coral", [255, 127, 80]], ["cornflowerblue", [100, 149, 237]], ["cornsilk", [255, 248, 220]], ["crimson", [220, 20, 60]], ["darkblue", [0, 0, 139]], ["darkcyan", [0, 139, 139]], ["darkgoldenrod", [184, 134, 11]], ["darkgray[*]", [169, 169, 169]], ["darkgreen", [0, 100, 0]], ["darkgrey[*]", [169, 169, 169]], ["darkkhaki", [189, 183, 107]], ["darkmagenta", [139, 0, 139]], ["darkolivegreen", [85, 107, 47]], ["darkorange", [255, 140, 0]], ["darkorchid", [153, 50, 204]], ["darkred", [139, 0, 0]], ["darksalmon", [233, 150, 122]], ["darkseagreen", [143, 188, 143]], ["darkslateblue", [72, 61, 139]], ["darkslategray", [47, 79, 79]], ["darkslategrey", [47, 79, 79]], ["darkturquoise", [0, 206, 209]], ["darkviolet", [148, 0, 211]], ["deeppink", [255, 20, 147]], ["deepskyblue", [0, 191, 255]], ["dimgray", [105, 105, 105]], ["dimgrey", [105, 105, 105]], ["dodgerblue", [30, 144, 255]], ["firebrick", [178, 34, 34]], ["floralwhite", [255, 250, 240]], ["forestgreen", [34, 139, 34]], ["gainsboro", [220, 220, 220]], ["ghostwhite", [248, 248, 255]], ["gold", [255, 215, 0]], ["goldenrod", [218, 165, 32]], ["greenyellow", [173, 255, 47]], ["grey", [128, 128, 128]], ["honeydew", [240, 255, 240]], ["hotpink", [255, 105, 180]], ["indianred", [205, 92, 92]], ["indigo", [75, 0, 130]], ["ivory", [255, 255, 240]], ["khaki", [240, 230, 140]], ["lavender", [230, 230, 250]], ["lavenderblush", [255, 240, 245]], ["lawngreen", [124, 252, 0]], ["lemonchiffon", [255, 250, 205]], ["lightblue", [173, 216, 230]], ["lightcoral", [240, 128, 128]], ["lightcyan", [224, 255, 255]], ["lightgoldenrodyellow", [250, 250, 210]], ["lightgray", [211, 211, 211]], ["lightgreen", [144, 238, 144]], ["lightgrey", [211, 211, 211]], ["lightpink", [255, 182, 193]], ["lightsalmon", [255, 160, 122]], ["lightseagreen", [32, 178, 170]], ["lightskyblue", [135, 206, 250]], ["lightslategray", [119, 136, 153]], ["lightslategrey", [119, 136, 153]], ["lightsteelblue", [176, 196, 222]], ["lightyellow", [255, 255, 224]], ["limegreen", [50, 205, 50]], ["linen", [250, 240, 230]], ["mediumaquamarine", [102, 205, 170]], ["mediumblue", [0, 0, 205]], ["mediumorchid", [186, 85, 211]], ["mediumpurple", [147, 112, 219]], ["mediumseagreen", [60, 179, 113]], ["mediumslateblue", [123, 104, 238]], ["mediumspringgreen", [0, 250, 154]], ["mediumturquoise", [72, 209, 204]], ["mediumvioletred", [199, 21, 133]], ["midnightblue", [25, 25, 112]], ["mintcream", [245, 255, 250]], ["mistyrose", [255, 228, 225]], ["moccasin", [255, 228, 181]], ["navajowhite", [255, 222, 173]], ["oldlace", [253, 245, 230]], ["olivedrab", [107, 142, 35]], ["orangered", [255, 69, 0]], ["orchid", [218, 112, 214]], ["palegoldenrod", [238, 232, 170]], ["palegreen", [152, 251, 152]], ["paleturquoise", [175, 238, 238]], ["palevioletred", [219, 112, 147]], ["papayawhip", [255, 239, 213]], ["peachpuff", [255, 218, 185]], ["peru", [205, 133, 63]], ["pink", [255, 192, 203]], ["plum", [221, 160, 221]], ["powderblue", [176, 224, 230]], ["rosybrown", [188, 143, 143]], ["royalblue", [65, 105, 225]], ["saddlebrown", [139, 69, 19]], ["salmon", [250, 128, 114]], ["sandybrown", [244, 164, 96]], ["seagreen", [46, 139, 87]], ["seashell", [255, 245, 238]], ["sienna", [160, 82, 45]], ["skyblue", [135, 206, 235]], ["slateblue", [106, 90, 205]], ["slategray", [112, 128, 144]], ["slategrey", [112, 128, 144]], ["snow", [255, 250, 250]], ["springgreen", [0, 255, 127]], ["steelblue", [70, 130, 180]], ["tan", [210, 180, 140]], ["thistle", [216, 191, 216]], ["tomato", [255, 99, 71]], ["turquoise", [64, 224, 208]], ["violet", [238, 130, 238]], ["wheat", [245, 222, 179]], ["whitesmoke", [245, 245, 245]], ["yellowgreen", [154, 205, 50]]].forEach(function (value) {
+ cssToRGB.set(value[0], value[1]);
+ });
+ /**
+ * A color manipulation object.
+ * @class
+ * @extends me.Object
+ * @memberOf me
+ * @constructor
+ * @param {Float32Array|Number} [r=0] red component or array of color components
+ * @param {Number} [g=0] green component
+ * @param {Number} [b=0] blue component
+ * @param {Number} [alpha=1.0] alpha value
+ */
+
+ me.Color = me.Object.extend({
+ /**
+ * @ignore
+ */
+ init: function init(r, g, b, alpha) {
+ /**
+ * Color components in a Float32Array suitable for WebGL
+ * @name glArray
+ * @memberOf me.Color
+ * @type {Float32Array}
+ * @readonly
+ */
+ if (typeof this.glArray === "undefined") {
+ this.glArray = new Float32Array([0.0, 0.0, 0.0, 1.0]);
+ }
+
+ return this.setColor(r, g, b, alpha);
+ },
+
+ /**
+ * Set this color to the specified value.
+ * @name setColor
+ * @memberOf me.Color
+ * @function
+ * @param {Number} r red component [0 .. 255]
+ * @param {Number} g green component [0 .. 255]
+ * @param {Number} b blue component [0 .. 255]
+ * @param {Number} [alpha=1.0] alpha value [0.0 .. 1.0]
+ * @return {me.Color} Reference to this object for method chaining
+ */
+ setColor: function setColor(r, g, b, alpha) {
+ // Private initialization: copy Color value directly
+ if (r instanceof me.Color) {
+ this.glArray.set(r.glArray);
+ return r;
+ }
+
+ this.r = r;
+ this.g = g;
+ this.b = b;
+ this.alpha = alpha;
+ return this;
+ },
+
+ /**
+ * Create a new copy of this color object.
+ * @name clone
+ * @memberOf me.Color
+ * @function
+ * @return {me.Color} Reference to the newly cloned object
+ */
+ clone: function clone() {
+ return me.pool.pull("me.Color", this);
+ },
+
+ /**
+ * Copy a color object or CSS color into this one.
+ * @name copy
+ * @memberOf me.Color
+ * @function
+ * @param {me.Color|String} color
+ * @return {me.Color} Reference to this object for method chaining
+ */
+ copy: function copy(color) {
+ if (color instanceof me.Color) {
+ this.glArray.set(color.glArray);
+ return this;
+ }
+
+ return this.parseCSS(color);
+ },
+
+ /**
+ * Blend this color with the given one using addition.
+ * @name add
+ * @memberOf me.Color
+ * @function
+ * @param {me.Color} color
+ * @return {me.Color} Reference to this object for method chaining
+ */
+ add: function add(color) {
+ this.glArray[0] = me.Math.clamp(this.glArray[0] + color.glArray[0], 0, 1);
+ this.glArray[1] = me.Math.clamp(this.glArray[1] + color.glArray[1], 0, 1);
+ this.glArray[2] = me.Math.clamp(this.glArray[2] + color.glArray[2], 0, 1);
+ this.glArray[3] = (this.glArray[3] + color.glArray[3]) / 2;
+ return this;
+ },
+
+ /**
+ * Darken this color value by 0..1
+ * @name darken
+ * @memberOf me.Color
+ * @function
+ * @param {Number} scale
+ * @return {me.Color} Reference to this object for method chaining
+ */
+ darken: function darken(scale) {
+ scale = me.Math.clamp(scale, 0, 1);
+ this.glArray[0] *= scale;
+ this.glArray[1] *= scale;
+ this.glArray[2] *= scale;
+ return this;
+ },
+
+ /**
+ * Lighten this color value by 0..1
+ * @name lighten
+ * @memberOf me.Color
+ * @function
+ * @param {Number} scale
+ * @return {me.Color} Reference to this object for method chaining
+ */
+ lighten: function lighten(scale) {
+ scale = me.Math.clamp(scale, 0, 1);
+ this.glArray[0] = me.Math.clamp(this.glArray[0] + (1 - this.glArray[0]) * scale, 0, 1);
+ this.glArray[1] = me.Math.clamp(this.glArray[1] + (1 - this.glArray[1]) * scale, 0, 1);
+ this.glArray[2] = me.Math.clamp(this.glArray[2] + (1 - this.glArray[2]) * scale, 0, 1);
+ return this;
+ },
+
+ /**
+ * Generate random r,g,b values for this color object
+ * @name random
+ * @memberOf me.Color
+ * @function
+ * @return {me.Color} Reference to this object for method chaining
+ */
+ random: function random() {
+ return this.setColor(Math.random() * 256, Math.random() * 256, Math.random() * 256, this.alpha);
+ },
+
+ /**
+ * Return true if the r,g,b,a values of this color are equal with the
+ * given one.
+ * @name equals
+ * @memberOf me.Color
+ * @function
+ * @param {me.Color} color
+ * @return {Boolean}
+ */
+ equals: function equals(color) {
+ return this.glArray[0] === color.glArray[0] && this.glArray[1] === color.glArray[1] && this.glArray[2] === color.glArray[2] && this.glArray[3] === color.glArray[3];
+ },
+
+ /**
+ * Parse a CSS color string and set this color to the corresponding
+ * r,g,b values
+ * @name parseCSS
+ * @memberOf me.Color
+ * @function
+ * @param {String} color
+ * @return {me.Color} Reference to this object for method chaining
+ */
+ parseCSS: function parseCSS(cssColor) {
+ // TODO : Memoize this function by caching its input
+ if (cssToRGB.has(cssColor)) {
+ return this.setColor.apply(this, cssToRGB.get(cssColor));
+ }
+
+ return this.parseRGB(cssColor);
+ },
+
+ /**
+ * Parse an RGB or RGBA CSS color string
+ * @name parseRGB
+ * @memberOf me.Color
+ * @function
+ * @param {String} color
+ * @return {me.Color} Reference to this object for method chaining
+ */
+ parseRGB: function parseRGB(rgbColor) {
+ // TODO : Memoize this function by caching its input
+ var match = rgbaRx.exec(rgbColor);
+
+ if (match) {
+ return this.setColor(+match[1], +match[2], +match[3], +match[5]);
+ }
+
+ return this.parseHex(rgbColor);
+ },
+
+ /**
+ * Parse a Hex color ("#RGB", "#RGBA" or "#RRGGBB", "#RRGGBBAA" format) and set this color to
+ * the corresponding r,g,b,a values
+ * @name parseHex
+ * @memberOf me.Color
+ * @function
+ * @param {String} color
+ * @return {me.Color} Reference to this object for method chaining
+ */
+ parseHex: function parseHex(hexColor) {
+ // TODO : Memoize this function by caching its input
+ var match;
+
+ if (match = hex8Rx.exec(hexColor)) {
+ // #AARRGGBB
+ return this.setColor(parseInt(match[1], 16), parseInt(match[2], 16), parseInt(match[3], 16), (me.Math.clamp(parseInt(match[4], 16), 0, 255) / 255.0).toFixed(1));
+ }
+
+ if (match = hex6Rx.exec(hexColor)) {
+ // #RRGGBB
+ return this.setColor(parseInt(match[1], 16), parseInt(match[2], 16), parseInt(match[3], 16));
+ }
+
+ if (match = hex4Rx.exec(hexColor)) {
+ // #ARGB
+ return this.setColor(parseInt(match[1] + match[1], 16), parseInt(match[2] + match[2], 16), parseInt(match[3] + match[3], 16), (me.Math.clamp(parseInt(match[4] + match[4], 16), 0, 255) / 255.0).toFixed(1));
+ }
+
+ if (match = hex3Rx.exec(hexColor)) {
+ // #RGB
+ return this.setColor(parseInt(match[1] + match[1], 16), parseInt(match[2] + match[2], 16), parseInt(match[3] + match[3], 16));
+ }
+
+ throw new Error("invalid parameter: " + hexColor);
+ },
+
+ /**
+ * Returns the private glArray
+ * @ignore
+ */
+ toGL: function toGL() {
+ return this.glArray;
+ },
+
+ /**
+ * Get the color in "#RRGGBB" format
+ * @name toHex
+ * @memberOf me.Color
+ * @function
+ * @return {String}
+ */
+ toHex: function toHex() {
+ // TODO : Memoize this function by caching its result until any of
+ // the r,g,b,a values are changed
+ return "#" + _toHex(this.r) + _toHex(this.g) + _toHex(this.b);
+ },
+
+ /**
+ * Get the color in "#RRGGBBAA" format
+ * @name toHex8
+ * @memberOf me.Color
+ * @function
+ * @return {String}
+ */
+ toHex8: function toHex8() {
+ // TODO : Memoize this function by caching its result until any of
+ // the r,g,b,a values are changed
+ return "#" + _toHex(this.r) + _toHex(this.g) + _toHex(this.b) + _toHex(this.alpha * 255);
+ },
+
+ /**
+ * Get the color in "rgb(R,G,B)" format
+ * @name toRGB
+ * @memberOf me.Color
+ * @function
+ * @return {String}
+ */
+ toRGB: function toRGB() {
+ // TODO : Memoize this function by caching its result until any of
+ // the r,g,b,a values are changed
+ return "rgb(" + this.r + "," + this.g + "," + this.b + ")";
+ },
+
+ /**
+ * Get the color in "rgba(R,G,B,A)" format
+ * @name toRGBA
+ * @memberOf me.Color
+ * @function
+ * @return {String}
+ */
+ toRGBA: function toRGBA() {
+ // TODO : Memoize this function by caching its result until any of
+ // the r,g,b,a values are changed
+ return "rgba(" + this.r + "," + this.g + "," + this.b + "," + this.alpha + ")";
+ }
+ });
+ /**
+ * Color Red Component
+ * @type Number
+ * @name r
+ * @readonly
+ * @memberOf me.Color
+ */
+
+ Object.defineProperty(me.Color.prototype, "r", {
+ /**
+ * @ignore
+ */
+ get: function get() {
+ return ~~(this.glArray[0] * 255);
+ },
+
+ /**
+ * @ignore
+ */
+ set: function set(value) {
+ this.glArray[0] = me.Math.clamp(~~value || 0, 0, 255) / 255.0;
+ },
+ enumerable: true,
+ configurable: true
+ });
+ /**
+ * Color Green Component
+ * @type Number
+ * @name g
+ * @readonly
+ * @memberOf me.Color
+ */
+
+ Object.defineProperty(me.Color.prototype, "g", {
+ /**
+ * @ignore
+ */
+ get: function get() {
+ return ~~(this.glArray[1] * 255);
+ },
+
+ /**
+ * @ignore
+ */
+ set: function set(value) {
+ this.glArray[1] = me.Math.clamp(~~value || 0, 0, 255) / 255.0;
+ },
+ enumerable: true,
+ configurable: true
+ });
+ /**
+ * Color Blue Component
+ * @type Number
+ * @name b
+ * @readonly
+ * @memberOf me.Color
+ */
+
+ Object.defineProperty(me.Color.prototype, "b", {
+ /**
+ * @ignore
+ */
+ get: function get() {
+ return ~~(this.glArray[2] * 255);
+ },
+
+ /**
+ * @ignore
+ */
+ set: function set(value) {
+ this.glArray[2] = me.Math.clamp(~~value || 0, 0, 255) / 255.0;
+ },
+ enumerable: true,
+ configurable: true
+ });
+ /**
+ * Color Alpha Component
+ * @type Number
+ * @name alpha
+ * @readonly
+ * @memberOf me.Color
+ */
+
+ Object.defineProperty(me.Color.prototype, "alpha", {
+ /**
+ * @ignore
+ */
+ get: function get() {
+ return this.glArray[3];
+ },
+
+ /**
+ * @ignore
+ */
+ set: function set(value) {
+ this.glArray[3] = typeof value === "undefined" ? 1.0 : me.Math.clamp(+value, 0, 1.0);
+ },
+ enumerable: true,
+ configurable: true
+ });
+ })();
+
+ (function () {
+ /**
+ * A singleton object to access the device localStorage area
+ * @example
+ * // Initialize "score" and "lives" with default values
+ * // This loads the properties from localStorage if they exist, else it sets the given defaults
+ * me.save.add({ score : 0, lives : 3 });
+ *
+ * // Print all
+ * // On first load, this prints { score : 0, lives : 3 }
+ * // On further reloads, it prints { score : 31337, lives : 3, complexObject : ... }
+ * // Because the following changes will be saved to localStorage
+ * console.log(JSON.stringify(me.save));
+ *
+ * // Save score
+ * me.save.score = 31337;
+ *
+ * // Also supports complex objects thanks to the JSON backend
+ * me.save.add({ complexObject : {} })
+ * me.save.complexObject = { a : "b", c : [ 1, 2, 3, "d" ], e : { f : [{}] } };
+ *
+ * // WARNING: Do not set any child properties of complex objects directly!
+ * // Changes made that way will not save. Always set the entire object value at once.
+ * // If you cannot live with this limitation, there's a workaround:
+ * me.save.complexObject.c.push("foo"); // Modify a child property
+ * me.save.complexObject = me.save.complexObject; // Save the entire object!
+ *
+ * // Remove "lives" from localStorage
+ * me.save.remove("lives");
+ * @namespace me.save
+ * @memberOf me
+ */
+ me.save = function () {
+ // Variable to hold the object data
+ var data = {}; // a function to check if the given key is a reserved word
+
+ function isReserved(key) {
+ return key === "add" || key === "remove";
+ } // Public API
+
+
+ var api = {
+ /**
+ * @ignore
+ */
+ _init: function _init() {
+ // Load previous data if local Storage is supported
+ if (me.device.localStorage === true) {
+ var me_save_content = localStorage.getItem("me.save");
+
+ if (typeof me_save_content === "string" && me_save_content.length > 0) {
+ var keys = JSON.parse(me_save_content) || [];
+ keys.forEach(function (key) {
+ data[key] = JSON.parse(localStorage.getItem("me.save." + key));
+ });
+ }
+ }
+ },
+
+ /**
+ * Add new keys to localStorage and set them to the given default values if they do not exist
+ * @name add
+ * @memberOf me.save
+ * @function
+ * @param {Object} props key and corresponding values
+ * @example
+ * // Initialize "score" and "lives" with default values
+ * me.save.add({ score : 0, lives : 3 });
+ */
+ add: function add(props) {
+ Object.keys(props).forEach(function (key) {
+ if (isReserved(key)) {
+ return;
+ }
+
+ (function (prop) {
+ Object.defineProperty(api, prop, {
+ configurable: true,
+ enumerable: true,
+
+ /**
+ * @ignore
+ */
+ get: function get() {
+ return data[prop];
+ },
+
+ /**
+ * @ignore
+ */
+ set: function set(value) {
+ data[prop] = value;
+
+ if (me.device.localStorage === true) {
+ localStorage.setItem("me.save." + prop, JSON.stringify(value));
+ }
+ }
+ });
+ })(key); // Set default value for key
+
+
+ if (!(key in data)) {
+ api[key] = props[key];
+ }
+ }); // Save keys
+
+ if (me.device.localStorage === true) {
+ localStorage.setItem("me.save", JSON.stringify(Object.keys(data)));
+ }
+ },
+
+ /**
+ * Remove a key from localStorage
+ * @name remove
+ * @memberOf me.save
+ * @function
+ * @param {String} key key to be removed
+ * @example
+ * // Remove the "score" key from localStorage
+ * me.save.remove("score");
+ */
+ remove: function remove(key) {
+ if (!isReserved(key)) {
+ if (typeof data[key] !== "undefined") {
+ delete data[key];
+
+ if (me.device.localStorage === true) {
+ localStorage.removeItem("me.save." + key);
+ localStorage.setItem("me.save", JSON.stringify(Object.keys(data)));
+ }
+ }
+ }
+ }
+ };
+ return api;
+ }();
+ })();
+
+ (function () {
+ /**
+ * a collection of TMX utility Function
+ * @final
+ * @memberOf me
+ * @ignore
+ */
+ me.TMXUtils = function () {
+ /*
+ * PUBLIC
+ */
+ // hold public stuff in our singleton
+ var api = {};
+ /**
+ * set and interpret a TMX property value
+ * @ignore
+ */
+
+ function setTMXValue(name, type, value) {
+ var match;
+
+ if (typeof value !== "string") {
+ // Value is already normalized (e.g. with JSON maps)
+ return value;
+ }
+
+ switch (type) {
+ case "int":
+ case "float":
+ value = Number(value);
+ break;
+
+ case "bool":
+ value = value === "true";
+ break;
+
+ default:
+ // try to parse it anyway
+ if (!value || me.utils.string.isBoolean(value)) {
+ // if value not defined or boolean
+ value = value ? value === "true" : true;
+ } else if (me.utils.string.isNumeric(value)) {
+ // check if numeric
+ value = Number(value);
+ } else if (value.search(/^json:/i) === 0) {
+ // try to parse it
+ match = value.split(/^json:/i)[1];
+
+ try {
+ value = JSON.parse(match);
+ } catch (e) {
+ throw new Error("Unable to parse JSON: " + match);
+ }
+ } else if (value.search(/^eval:/i) === 0) {
+ // try to evaluate it
+ match = value.split(/^eval:/i)[1];
+
+ try {
+ // eslint-disable-next-line
+ value = eval(match);
+ } catch (e) {
+ throw new Error("Unable to evaluate: " + match);
+ }
+ } else if ((match = value.match(/^#([\da-fA-F])([\da-fA-F]{3})$/)) || (match = value.match(/^#([\da-fA-F]{2})([\da-fA-F]{6})$/))) {
+ value = "#" + match[2] + match[1];
+ } // normalize values
+
+
+ if (name.search(/^(ratio|anchorPoint)$/) === 0) {
+ // convert number to vector
+ if (typeof value === "number") {
+ value = {
+ "x": value,
+ "y": value
+ };
+ }
+ }
+
+ } // return the interpreted value
+
+
+ return value;
+ }
+
+ function parseAttributes(obj, elt) {
+ // do attributes
+ if (elt.attributes && elt.attributes.length > 0) {
+ for (var j = 0; j < elt.attributes.length; j++) {
+ var attribute = elt.attributes.item(j);
+
+ if (typeof attribute.name !== "undefined") {
+ // DOM4 (Attr no longer inherit from Node)
+ obj[attribute.name] = attribute.value;
+ } else {
+ // else use the deprecated ones
+ obj[attribute.nodeName] = attribute.nodeValue;
+ }
+ }
+ }
+ }
+ /**
+ * decompress zlib/gzip data (NOT IMPLEMENTED)
+ * @ignore
+ * @function
+ * @memberOf me.TMXUtils
+ * @name decompress
+ * @param {Number[]} data Array of bytes
+ * @param {String} format compressed data format ("gzip","zlib")
+ * @return {Number[]} Decompressed data
+ */
+
+
+ api.decompress = function () {
+ throw new Error("GZIP/ZLIB compressed TMX Tile Map not supported!");
+ };
+ /**
+ * Decode a CSV encoded array into a binary array
+ * @ignore
+ * @function
+ * @memberOf me.TMXUtils
+ * @name decodeCSV
+ * @param {String} input CSV formatted data (only numbers, everything else will be converted to NaN)
+ * @return {Number[]} Decoded data
+ */
+
+
+ api.decodeCSV = function (input) {
+ var entries = input.replace("\n", "").trim().split(",");
+ var result = [];
+
+ for (var i = 0; i < entries.length; i++) {
+ result.push(+entries[i]);
+ }
+
+ return result;
+ };
+ /**
+ * Decode a base64 encoded string into a byte array
+ * @ignore
+ * @function
+ * @memberOf me.TMXUtils
+ * @name decodeBase64AsArray
+ * @param {String} input Base64 encoded data
+ * @param {Number} [bytes] number of bytes per array entry
+ * @return {Uint32Array} Decoded data
+ */
+
+
+ api.decodeBase64AsArray = function (input, bytes) {
+ bytes = bytes || 1;
+ var i, j, len;
+ var dec = window.atob(input.replace(/[^A-Za-z0-9\+\/\=]/g, ""));
+ var ar = new Uint32Array(dec.length / bytes);
+
+ for (i = 0, len = dec.length / bytes; i < len; i++) {
+ ar[i] = 0;
+
+ for (j = bytes - 1; j >= 0; --j) {
+ ar[i] += dec.charCodeAt(i * bytes + j) << (j << 3);
+ }
+ }
+
+ return ar;
+ };
+ /**
+ * Decode the given data
+ * @ignore
+ */
+
+
+ api.decode = function (data, encoding, compression) {
+ compression = compression || "none";
+ encoding = encoding || "none";
+
+ switch (encoding) {
+ case "csv":
+ return api.decodeCSV(data);
+
+ case "base64":
+ var decoded = api.decodeBase64AsArray(data, 4);
+ return compression === "none" ? decoded : api.decompress(decoded, compression);
+
+ case "none":
+ return data;
+
+ case "xml":
+ throw new Error("XML encoding is deprecated, use base64 instead");
+
+ default:
+ throw new Error("Unknown layer encoding: " + encoding);
+ }
+ };
+ /**
+ * Normalize TMX format to Tiled JSON format
+ * @ignore
+ */
+
+
+ api.normalize = function (obj, item) {
+ var nodeName = item.nodeName;
+
+ switch (nodeName) {
+ case "data":
+ var data = api.parse(item); // #956 Support for Infinite map
+ // workaround to prevent the parsing code from crashing
+
+ data.text = data.text || data.chunk.text; // When no encoding is given, the tiles are stored as individual XML tile elements.
+
+ data.encoding = data.encoding || "xml";
+ obj.data = api.decode(data.text, data.encoding, data.compression);
+ obj.encoding = "none";
+ break;
+
+ case "imagelayer":
+ case "layer":
+ case "objectgroup":
+ case "group":
+ var layer = api.parse(item);
+ layer.type = nodeName === "layer" ? "tilelayer" : nodeName;
+
+ if (layer.image) {
+ layer.image = layer.image.source;
+ }
+
+ obj.layers = obj.layers || [];
+ obj.layers.push(layer);
+ break;
+
+ case "animation":
+ obj.animation = api.parse(item).frames;
+ break;
+
+ case "frame":
+ case "object":
+ var name = nodeName + "s";
+ obj[name] = obj[name] || [];
+ obj[name].push(api.parse(item));
+ break;
+
+ case "tile":
+ var tile = api.parse(item);
+
+ if (tile.image) {
+ tile.imagewidth = tile.image.width;
+ tile.imageheight = tile.image.height;
+ tile.image = tile.image.source;
+ }
+
+ obj.tiles = obj.tiles || {};
+ obj.tiles[tile.id] = tile;
+ break;
+
+ case "tileset":
+ var tileset = api.parse(item);
+
+ if (tileset.image) {
+ tileset.imagewidth = tileset.image.width;
+ tileset.imageheight = tileset.image.height;
+ tileset.image = tileset.image.source;
+ }
+
+ obj.tilesets = obj.tilesets || [];
+ obj.tilesets.push(tileset);
+ break;
+
+ case "polygon":
+ case "polyline":
+ obj[nodeName] = []; // Get a point array
+
+ var points = api.parse(item).points.split(" "); // And normalize them into an array of vectors
+
+ for (var i = 0, v; i < points.length; i++) {
+ v = points[i].split(",");
+ obj[nodeName].push({
+ "x": +v[0],
+ "y": +v[1]
+ });
+ }
+
+ break;
+
+ case "properties":
+ obj.properties = api.parse(item);
+ break;
+
+ case "property":
+ var property = api.parse(item);
+ obj[property.name] = setTMXValue(property.name, // in XML type is undefined for "string" values
+ property.type || "string", property.value);
+ break;
+
+ default:
+ obj[nodeName] = api.parse(item);
+ break;
+ }
+ };
+ /**
+ * Parse a XML TMX object and returns the corresponding javascript object
+ * @ignore
+ */
+
+
+ api.parse = function (xml) {
+ // Create the return object
+ var obj = {};
+ var text = "";
+
+ if (xml.nodeType === 1) {
+ // do attributes
+ parseAttributes(obj, xml);
+ } // do children
+
+
+ if (xml.hasChildNodes()) {
+ for (var i = 0; i < xml.childNodes.length; i++) {
+ var item = xml.childNodes.item(i);
+
+ switch (item.nodeType) {
+ case 1:
+ api.normalize(obj, item);
+ break;
+
+ case 3:
+ text += item.nodeValue.trim();
+ break;
+ }
+ }
+ }
+
+ if (text) {
+ obj.text = text;
+ }
+
+ return obj;
+ };
+ /**
+ * Apply TMX Properties to the given object
+ * @ignore
+ */
+
+
+ api.applyTMXProperties = function (obj, data) {
+ var properties = data.properties;
+ var types = data.propertytypes;
+
+ if (typeof properties !== "undefined") {
+ for (var property in properties) {
+ if (properties.hasOwnProperty(property)) {
+ var type = "string";
+ var name = property;
+ var value = properties[property]; // proof-check for new and old JSON format
+
+ if (typeof properties[property].name !== "undefined") {
+ name = properties[property].name;
+ }
+
+ if (typeof types !== "undefined") {
+ type = types[property];
+ } else if (typeof properties[property].type !== "undefined") {
+ type = properties[property].type;
+ }
+
+ if (typeof properties[property].value !== "undefined") {
+ value = properties[property].value;
+ } // set the value
+
+
+ obj[name] = setTMXValue(name, type, value);
+ }
+ }
+ }
+ }; // return our object
+
+
+ return api;
+ }();
+ })();
+
+ (function () {
+ /**
+ * TMX Group
+ * contains an object group definition as defined in Tiled.
+ * note : object group definition is translated into the virtual `me.game.world` using `me.Container`.
+ * @see me.Container
+ * @class
+ * @extends me.Object
+ * @memberOf me
+ * @constructor
+ */
+ me.TMXGroup = me.Object.extend({
+ /**
+ * @ignore
+ */
+ init: function init(map, data, z) {
+ /**
+ * group name
+ * @public
+ * @type String
+ * @name name
+ * @memberOf me.TMXGroup
+ */
+ this.name = data.name;
+ /**
+ * group width
+ * @public
+ * @type Number
+ * @name width
+ * @memberOf me.TMXGroup
+ */
+
+ this.width = data.width || 0;
+ /**
+ * group height
+ * @public
+ * @type Number
+ * @name height
+ * @memberOf me.TMXGroup
+ */
+
+ this.height = data.height || 0;
+ /**
+ * group z order
+ * @public
+ * @type Number
+ * @name z
+ * @memberOf me.TMXGroup
+ */
+
+ this.z = z;
+ /**
+ * group objects list definition
+ * @see me.TMXObject
+ * @public
+ * @type Array
+ * @name name
+ * @memberOf me.TMXGroup
+ */
+
+ this.objects = [];
+ var visible = typeof data.visible !== "undefined" ? data.visible : true;
+ this.opacity = visible === true ? me.Math.clamp(+data.opacity || 1.0, 0.0, 1.0) : 0; // check if we have any user-defined properties
+
+ me.TMXUtils.applyTMXProperties(this, data); // parse all child objects/layers
+
+ var self = this;
+
+ if (data.objects) {
+ var _objects = data.objects;
+
+ _objects.forEach(function (object) {
+ self.objects.push(new me.TMXObject(map, object, z));
+ });
+ }
+
+ if (data.layers) {
+ var _layers = data.layers;
+
+ _layers.forEach(function (data) {
+ var layer = new me.TMXLayer(data, map.tilewidth, map.tileheight, map.orientation, map.tilesets, z++); // set a renderer
+
+ layer.setRenderer(map.getRenderer(layer)); // resize container accordingly
+
+ self.width = Math.max(self.width, layer.width);
+ self.height = Math.max(self.height, layer.height);
+ self.objects.push(layer);
+ });
+ }
+ },
+
+ /**
+ * reset function
+ * @ignore
+ * @function
+ */
+ destroy: function destroy() {
+ // clear all allocated objects
+ this.objects = null;
+ },
+
+ /**
+ * return the object count
+ * @ignore
+ * @function
+ */
+ getObjectCount: function getObjectCount() {
+ return this.objects.length;
+ },
+
+ /**
+ * returns the object at the specified index
+ * @ignore
+ * @function
+ */
+ getObjectByIndex: function getObjectByIndex(idx) {
+ return this.objects[idx];
+ }
+ });
+ })();
+
+ (function () {
+ /**
+ * a TMX Object defintion, as defined in Tiled.
+ * note : object definition are translated into the virtual `me.game.world` using `me.Entity`.
+ * @see me.Entity
+ * @class
+ * @extends me.Object
+ * @memberOf me
+ * @constructor
+ */
+ me.TMXObject = me.Object.extend({
+ /**
+ * @ignore
+ */
+ init: function init(map, settings, z) {
+ /**
+ * point list in JSON format
+ * @public
+ * @type Object[]
+ * @name points
+ * @memberOf me.TMXObject
+ */
+ this.points = undefined;
+ /**
+ * object name
+ * @public
+ * @type String
+ * @name name
+ * @memberOf me.TMXObject
+ */
+
+ this.name = settings.name;
+ /**
+ * object x position
+ * @public
+ * @type Number
+ * @name x
+ * @memberOf me.TMXObject
+ */
+
+ this.x = +settings.x;
+ /**
+ * object y position
+ * @public
+ * @type Number
+ * @name y
+ * @memberOf me.TMXObject
+ */
+
+ this.y = +settings.y;
+ /**
+ * object z order
+ * @public
+ * @type Number
+ * @name z
+ * @memberOf me.TMXObject
+ */
+
+ this.z = +z;
+ /**
+ * object width
+ * @public
+ * @type Number
+ * @name width
+ * @memberOf me.TMXObject
+ */
+
+ this.width = +settings.width || 0;
+ /**
+ * object height
+ * @public
+ * @type Number
+ * @name height
+ * @memberOf me.TMXObject
+ */
+
+ this.height = +settings.height || 0;
+ /**
+ * object gid value
+ * when defined the object is a tiled object
+ * @public
+ * @type Number
+ * @name gid
+ * @memberOf me.TMXObject
+ */
+
+ this.gid = +settings.gid || null;
+ /**
+ * object type
+ * @public
+ * @type String
+ * @name type
+ * @memberOf me.TMXObject
+ */
+
+ this.type = settings.type;
+ /**
+ * object text
+ * @public
+ * @type Object
+ * @see http://docs.mapeditor.org/en/stable/reference/tmx-map-format/#text
+ * @name type
+ * @memberOf me.TMXObject
+ */
+
+ this.type = settings.type;
+ /**
+ * The rotation of the object in radians clockwise (defaults to 0)
+ * @public
+ * @type Number
+ * @name rotation
+ * @memberOf me.TMXObject
+ */
+
+ this.rotation = me.Math.degToRad(+settings.rotation || 0);
+ /**
+ * object unique identifier per level (Tiled 0.11.x+)
+ * @public
+ * @type Number
+ * @name id
+ * @memberOf me.TMXObject
+ */
+
+ this.id = +settings.id || undefined;
+ /**
+ * object orientation (orthogonal or isometric)
+ * @public
+ * @type String
+ * @name orientation
+ * @memberOf me.TMXObject
+ */
+
+ this.orientation = map.orientation;
+ /**
+ * the collision shapes defined for this object
+ * @public
+ * @type Array
+ * @name shapes
+ * @memberOf me.TMXObject
+ */
+
+ this.shapes = undefined;
+ /**
+ * if true, the object is an Ellipse
+ * @public
+ * @type Boolean
+ * @name isEllipse
+ * @memberOf me.TMXObject
+ */
+
+ this.isEllipse = false;
+ /**
+ * if true, the object is a Polygon
+ * @public
+ * @type Boolean
+ * @name isPolygon
+ * @memberOf me.TMXObject
+ */
+
+ this.isPolygon = false;
+ /**
+ * if true, the object is a PolyLine
+ * @public
+ * @type Boolean
+ * @name isPolyLine
+ * @memberOf me.TMXObject
+ */
+
+ this.isPolyLine = false; // check if the object has an associated gid
+
+ if (typeof this.gid === "number") {
+ this.setTile(map.tilesets);
+ } else {
+ if (typeof settings.ellipse !== "undefined") {
+ this.isEllipse = true;
+ } else if (typeof settings.polygon !== "undefined") {
+ this.points = settings.polygon;
+ this.isPolygon = true;
+ } else if (typeof settings.polyline !== "undefined") {
+ this.points = settings.polyline;
+ this.isPolyLine = true;
+ }
+ } // check for text information
+
+
+ if (typeof settings.text !== "undefined") {
+ // a text object
+ this.text = settings.text; // normalize field name and default value the melonjs way
+
+ this.text.font = settings.text.fontfamily || "sans-serif";
+ this.text.size = settings.text.pixelsize || 16;
+ this.text.fillStyle = settings.text.color || "#000000";
+ this.text.textAlign = settings.text.halign || "left";
+ this.text.textBaseline = settings.text.valign || "top";
+ this.text.width = this.width;
+ this.text.height = this.height; // set the object properties
+
+ me.TMXUtils.applyTMXProperties(this.text, settings);
+ } else {
+ // set the object properties
+ me.TMXUtils.applyTMXProperties(this, settings); // a standard object
+
+ if (!this.shapes) {
+ // else define the object shapes if required
+ this.shapes = this.parseTMXShapes();
+ }
+ } // Adjust the Position to match Tiled
+
+
+ if (!map.isEditor) {
+ map.getRenderer().adjustPosition(this);
+ }
+ },
+
+ /**
+ * set the object image (for Tiled Object)
+ * @ignore
+ * @function
+ */
+ setTile: function setTile(tilesets) {
+ // get the corresponding tileset
+ var tileset = tilesets.getTilesetByGid(this.gid);
+
+ if (tileset.isCollection === false) {
+ // set width and height equal to tile size
+ this.width = this.framewidth = tileset.tilewidth;
+ this.height = this.frameheight = tileset.tileheight;
+ } // the object corresponding tile object
+
+
+ this.tile = new me.Tile(this.x, this.y, this.gid, tileset);
+ },
+
+ /**
+ * parses the TMX shape definition and returns a corresponding array of me.Shape object
+ * @name parseTMXShapes
+ * @memberOf me.TMXObject
+ * @private
+ * @function
+ * @return {me.Polygon[]|me.Line[]|me.Ellipse[]} an array of shape objects
+ */
+ parseTMXShapes: function parseTMXShapes() {
+ var i = 0;
+ var shapes = []; // add an ellipse shape
+
+ if (this.isEllipse === true) {
+ // ellipse coordinates are the center position, so set default to the corresonding radius
+ shapes.push(new me.Ellipse(this.width / 2, this.height / 2, this.width, this.height).rotate(this.rotation));
+ } // add a polygon
+ else if (this.isPolygon === true) {
+ shapes.push(new me.Polygon(0, 0, this.points).rotate(this.rotation));
+ } // add a polyline
+ else if (this.isPolyLine === true) {
+ var p = this.points;
+ var p1, p2;
+ var segments = p.length - 1;
+
+ for (i = 0; i < segments; i++) {
+ // clone the value before, as [i + 1]
+ // is reused later by the next segment
+ p1 = new me.Vector2d(p[i].x, p[i].y);
+ p2 = new me.Vector2d(p[i + 1].x, p[i + 1].y);
+
+ if (this.rotation !== 0) {
+ p1 = p1.rotate(this.rotation);
+ p2 = p2.rotate(this.rotation);
+ }
+
+ shapes.push(new me.Line(0, 0, [p1, p2]));
+ }
+ } // it's a rectangle, returns a polygon object anyway
+ else {
+ shapes.push(new me.Polygon(0, 0, [new me.Vector2d(), new me.Vector2d(this.width, 0), new me.Vector2d(this.width, this.height), new me.Vector2d(0, this.height)]).rotate(this.rotation));
+ } // Apply isometric projection
+
+
+ if (this.orientation === "isometric") {
+ for (i = 0; i < shapes.length; i++) {
+ shapes[i].toIso();
+ }
+ }
+
+ return shapes;
+ },
+
+ /**
+ * getObjectPropertyByName
+ * @ignore
+ * @function
+ */
+ getObjectPropertyByName: function getObjectPropertyByName(name) {
+ return this[name];
+ }
+ });
+ })();
+
+ (function () {
+ // bitmask constants to check for flipped & rotated tiles
+ var TMX_FLIP_H = 0x80000000,
+ TMX_FLIP_V = 0x40000000,
+ TMX_FLIP_AD = 0x20000000,
+ TMX_CLEAR_BIT_MASK = ~(0x80000000 | 0x40000000 | 0x20000000);
+ /**
+ * a basic tile object
+ * @class
+ * @extends me.Rect
+ * @memberOf me
+ * @constructor
+ * @param {Number} x x index of the Tile in the map
+ * @param {Number} y y index of the Tile in the map
+ * @param {Number} gid tile gid
+ * @param {me.TMXTileset} tileset the corresponding tileset object
+ */
+
+ me.Tile = me.Rect.extend({
+ /** @ignore */
+ init: function init(x, y, gid, tileset) {
+ var width, height; // determine the tile size
+
+ if (tileset.isCollection) {
+ var image = tileset.getTileImage(gid & TMX_CLEAR_BIT_MASK);
+ width = image.width;
+ height = image.height;
+ } else {
+ width = tileset.tilewidth;
+ height = tileset.tileheight;
+ } // call the parent constructor
+
+
+ this._super(me.Rect, "init", [x * width, y * height, width, height]);
+ /**
+ * tileset
+ * @public
+ * @type me.TMXTileset
+ * @name me.Tile#tileset
+ */
+
+
+ this.tileset = tileset;
+ /**
+ * the tile transformation matrix (if defined)
+ * @ignore
+ */
+
+ this.currentTransform = null; // Tile col / row pos
+
+ this.col = x;
+ this.row = y;
+ /**
+ * tileId
+ * @public
+ * @type Number
+ * @name me.Tile#tileId
+ */
+
+ this.tileId = gid;
+ /**
+ * True if the tile is flipped horizontally
+ * @public
+ * @type Boolean
+ * @name me.Tile#flipX
+ */
+
+ this.flippedX = (this.tileId & TMX_FLIP_H) !== 0;
+ /**
+ * True if the tile is flipped vertically
+ * @public
+ * @type Boolean
+ * @name me.Tile#flippedY
+ */
+
+ this.flippedY = (this.tileId & TMX_FLIP_V) !== 0;
+ /**
+ * True if the tile is flipped anti-diagonally
+ * @public
+ * @type Boolean
+ * @name me.Tile#flippedAD
+ */
+
+ this.flippedAD = (this.tileId & TMX_FLIP_AD) !== 0;
+ /**
+ * Global flag that indicates if the tile is flipped
+ * @public
+ * @type Boolean
+ * @name me.Tile#flipped
+ */
+
+ this.flipped = this.flippedX || this.flippedY || this.flippedAD; // create a transformation matrix if required
+
+ if (this.flipped === true) {
+ this.createTransform();
+ } // clear out the flags and set the tileId
+
+
+ this.tileId &= TMX_CLEAR_BIT_MASK;
+ },
+
+ /**
+ * create a transformation matrix for this tile
+ * @ignore
+ */
+ createTransform: function createTransform() {
+ if (this.currentTransform === null) {
+ this.currentTransform = new me.Matrix2d();
+ } else {
+ // reset the matrix
+ this.currentTransform.identity();
+ }
+
+ if (this.flippedAD) {
+ // Use shearing to swap the X/Y axis
+ this.currentTransform.setTransform(0, 1, 0, 1, 0, 0, 0, 0, 1);
+ this.currentTransform.translate(0, this.height - this.width);
+ }
+
+ if (this.flippedX) {
+ this.currentTransform.translate(this.flippedAD ? 0 : this.width, this.flippedAD ? this.height : 0);
+ this.currentTransform.scaleX(-1);
+ }
+
+ if (this.flippedY) {
+ this.currentTransform.translate(this.flippedAD ? this.width : 0, this.flippedAD ? 0 : this.height);
+ this.currentTransform.scaleY(-1);
+ }
+ },
+
+ /**
+ * return a renderable object for this Tile object
+ * @name me.Tile#getRenderable
+ * @public
+ * @function
+ * @param {Object} [settings] see {@link me.Sprite}
+ * @return {me.Renderable} a me.Sprite object
+ */
+ getRenderable: function getRenderable(settings) {
+ var renderable;
+ var tileset = this.tileset;
+
+ if (tileset.animations.has(this.tileId)) {
+ var frames = [];
+ var frameId = [];
+ tileset.animations.get(this.tileId).frames.forEach(function (frame) {
+ frameId.push(frame.tileid);
+ frames.push({
+ name: "" + frame.tileid,
+ delay: frame.duration
+ });
+ });
+ renderable = tileset.texture.createAnimationFromName(frameId, settings);
+ renderable.addAnimation(this.tileId - tileset.firstgid, frames);
+ renderable.setCurrentAnimation(this.tileId - tileset.firstgid);
+ } else {
+ if (tileset.isCollection === true) {
+ var image = tileset.getTileImage(this.tileId);
+ renderable = new me.Sprite(0, 0, Object.assign({
+ image: image
+ }) //, settings)
+ );
+ renderable.anchorPoint.set(0, 0);
+ renderable.scale(settings.width / this.width, settings.height / this.height);
+
+ if (typeof settings.rotation !== "undefined") {
+ renderable.anchorPoint.set(0.5, 0.5);
+ renderable.currentTransform.rotate(settings.rotation);
+ renderable.currentTransform.translate(settings.width / 2, settings.height / 2); // TODO : move the rotation related code from TMXTiledMap to here (under)
+
+ settings.rotation = undefined;
+ }
+ } else {
+ renderable = tileset.texture.createSpriteFromName(this.tileId - tileset.firstgid, settings);
+ }
+ } // any H/V flipping to apply?
+
+
+ if (this.flippedX) {
+ renderable.currentTransform.scaleX(-1);
+ }
+
+ if (this.flippedY) {
+ renderable.currentTransform.scaleY(-1);
+ }
+
+ return renderable;
+ }
+ });
+ })();
+
+ (function () {
+ // bitmask constants to check for flipped & rotated tiles
+ var TMX_CLEAR_BIT_MASK = ~(0x80000000 | 0x40000000 | 0x20000000);
+ /**
+ * a TMX Tile Set Object
+ * @class
+ * @extends me.Object
+ * @memberOf me
+ * @constructor
+ * @param {Object} tileset tileset data in JSON format ({@link http://docs.mapeditor.org/en/stable/reference/tmx-map-format/#tileset})
+ */
+
+ me.TMXTileset = me.Object.extend({
+ /**
+ * constructor
+ * @ignore
+ */
+ init: function init(tileset) {
+ var i = 0; // first gid
+ // tile properties (collidable, etc..)
+
+ this.TileProperties = []; // hold reference to each tile image
+
+ this.imageCollection = [];
+ this.firstgid = this.lastgid = +tileset.firstgid; // check if an external tileset is defined
+
+ if (typeof tileset.source !== "undefined") {
+ var src = tileset.source;
+ var ext = me.utils.file.getExtension(src);
+
+ if (ext === "tsx" || ext === "json") {
+ // load the external tileset (TSX/JSON)
+ tileset = me.loader.getTMX(me.utils.file.getBasename(src));
+
+ if (!tileset) {
+ throw new Error(src + " external TSX/JSON tileset not found");
+ }
+ }
+ }
+
+ this.name = tileset.name;
+ this.tilewidth = +tileset.tilewidth;
+ this.tileheight = +tileset.tileheight;
+ this.spacing = +tileset.spacing || 0;
+ this.margin = +tileset.margin || 0; // set tile offset properties (if any)
+
+ this.tileoffset = new me.Vector2d();
+ /**
+ * Tileset contains animated tiles
+ * @public
+ * @type Boolean
+ * @name me.TMXTileset#isAnimated
+ */
+
+ this.isAnimated = false;
+ /**
+ * true if the tileset is a "Collection of Image" Tileset
+ * @public
+ * @type Boolean
+ * @name me.TMXTileset#isCollection
+ */
+
+ this.isCollection = false;
+ /**
+ * Tileset animations
+ * @private
+ * @type Map
+ * @name me.TMXTileset#animations
+ */
+
+ this.animations = new Map();
+ /**
+ * Remember the last update timestamp to prevent too many animation updates
+ * @private
+ * @type Map
+ * @name me.TMXTileset#_lastUpdate
+ */
+
+ this._lastUpdate = 0;
+ var tiles = tileset.tiles;
+
+ for (i in tiles) {
+ if (tiles.hasOwnProperty(i)) {
+ if ("animation" in tiles[i]) {
+ this.isAnimated = true;
+ this.animations.set(+i + this.firstgid, {
+ dt: 0,
+ idx: 0,
+ frames: tiles[i].animation,
+ cur: tiles[i].animation[0]
+ });
+ } // set tile properties, if any (XML format)
+
+
+ if ("properties" in tiles[i]) {
+ this.setTileProperty(+i + this.firstgid, tiles[i].properties);
+ }
+
+ if ("image" in tiles[i]) {
+ var image = me.loader.getImage(tiles[i].image);
+
+ if (!image) {
+ throw new Error("melonJS: '" + tiles[i].image + "' file for tile '" + (+i + this.firstgid) + "' not found!");
+ }
+
+ this.imageCollection[+i + this.firstgid] = image;
+ }
+ }
+ }
+
+ this.isCollection = this.imageCollection.length > 0;
+ var offset = tileset.tileoffset;
+
+ if (offset) {
+ this.tileoffset.x = +offset.x;
+ this.tileoffset.y = +offset.y;
+ } // set tile properties, if any (JSON format)
+
+
+ var tileInfo = tileset.tileproperties;
+
+ if (tileInfo) {
+ for (i in tileInfo) {
+ if (tileInfo.hasOwnProperty(i)) {
+ this.setTileProperty(+i + this.firstgid, tileInfo[i]);
+ }
+ }
+ } // if not a tile image collection
+
+
+ if (this.isCollection === false) {
+ // get the global tileset texture
+ this.image = me.loader.getImage(tileset.image);
+
+ if (!this.image) {
+ throw new Error("melonJS: '" + tileset.image + "' file for tileset '" + this.name + "' not found!");
+ } // create a texture atlas for the given tileset
+
+
+ this.texture = me.video.renderer.cache.get(this.image, {
+ framewidth: this.tilewidth,
+ frameheight: this.tileheight,
+ margin: this.margin,
+ spacing: this.spacing
+ });
+ this.atlas = this.texture.getAtlas(); // calculate the number of tiles per horizontal line
+
+ var hTileCount = +tileset.columns || ~~(this.image.width / (this.tilewidth + this.spacing));
+ var vTileCount = ~~(this.image.height / (this.tileheight + this.spacing)); // compute the last gid value in the tileset
+
+ this.lastgid = this.firstgid + (hTileCount * vTileCount - 1 || 0);
+
+ if (tileset.tilecount && this.lastgid - this.firstgid + 1 !== +tileset.tilecount) {
+ console.warn("Computed tilecount (" + (this.lastgid - this.firstgid + 1) + ") does not match expected tilecount (" + tileset.tilecount + ")");
+ }
+ }
+ },
+
+ /**
+ * return the tile image from a "Collection of Image" tileset
+ * @name me.TMXTileset#getTileImage
+ * @public
+ * @function
+ * @param {Number} gid
+ * @return {Image} corresponding image or undefined
+ */
+ getTileImage: function getTileImage(gid) {
+ return this.imageCollection[gid];
+ },
+
+ /**
+ * set the tile properties
+ * @ignore
+ * @function
+ */
+ setTileProperty: function setTileProperty(gid, prop) {
+ // set the given tile id
+ this.TileProperties[gid] = prop;
+ },
+
+ /**
+ * return true if the gid belongs to the tileset
+ * @name me.TMXTileset#contains
+ * @public
+ * @function
+ * @param {Number} gid
+ * @return {Boolean}
+ */
+ contains: function contains(gid) {
+ return gid >= this.firstgid && gid <= this.lastgid;
+ },
+
+ /**
+ * Get the view (local) tile ID from a GID, with animations applied
+ * @name me.TMXTileset#getViewTileId
+ * @public
+ * @function
+ * @param {Number} gid Global tile ID
+ * @return {Number} View tile ID
+ */
+ getViewTileId: function getViewTileId(gid) {
+ if (this.animations.has(gid)) {
+ // apply animations
+ gid = this.animations.get(gid).cur.tileid;
+ } else {
+ // get the local tileset id
+ gid -= this.firstgid;
+ }
+
+ return gid;
+ },
+
+ /**
+ * return the properties of the specified tile
+ * @name me.TMXTileset#getTileProperties
+ * @public
+ * @function
+ * @param {Number} tileId
+ * @return {Object}
+ */
+ getTileProperties: function getTileProperties(tileId) {
+ return this.TileProperties[tileId];
+ },
+ // update tile animations
+ update: function update(dt) {
+ var duration = 0,
+ now = me.timer.getTime(),
+ result = false;
+
+ if (this._lastUpdate !== now) {
+ this._lastUpdate = now;
+ this.animations.forEach(function (anim) {
+ anim.dt += dt;
+ duration = anim.cur.duration;
+
+ while (anim.dt >= duration) {
+ anim.dt -= duration;
+ anim.idx = (anim.idx + 1) % anim.frames.length;
+ anim.cur = anim.frames[anim.idx];
+ duration = anim.cur.duration;
+ result = true;
+ }
+ });
+ }
+
+ return result;
+ },
+ // draw the x,y tile
+ drawTile: function drawTile(renderer, dx, dy, tmxTile) {
+ // check if any transformation is required
+ if (tmxTile.flipped) {
+ renderer.save(); // apply the tile current transform
+
+ renderer.translate(dx, dy);
+ renderer.transform(tmxTile.currentTransform); // reset both values as managed through transform();
+
+ dx = dy = 0;
+ } // check if the tile has an associated image
+
+
+ if (this.isCollection === true) {
+ // draw the tile
+ renderer.drawImage(this.imageCollection[tmxTile.tileId], 0, 0, tmxTile.width, tmxTile.height, dx, dy, tmxTile.width, tmxTile.height);
+ } else {
+ // use the tileset texture
+ var offset = this.atlas[this.getViewTileId(tmxTile.tileId)].offset; // draw the tile
+
+ renderer.drawImage(this.image, offset.x, offset.y, this.tilewidth, this.tileheight, dx, dy, this.tilewidth + renderer.uvOffset, this.tileheight + renderer.uvOffset);
+ }
+
+ if (tmxTile.flipped) {
+ // restore the context to the previous state
+ renderer.restore();
+ }
+ }
+ });
+ /**
+ * an object containing all tileset
+ * @class
+ * @memberOf me
+ * @constructor
+ */
+
+ me.TMXTilesetGroup = me.Object.extend({
+ /**
+ * constructor
+ * @ignore
+ */
+ init: function init() {
+ this.tilesets = [];
+ this.length = 0;
+ },
+
+ /**
+ * add a tileset to the tileset group
+ * @name me.TMXTilesetGroup#add
+ * @public
+ * @function
+ * @param {me.TMXTileset} tileset
+ */
+ add: function add(tileset) {
+ this.tilesets.push(tileset);
+ this.length++;
+ },
+
+ /**
+ * return the tileset at the specified index
+ * @name me.TMXTilesetGroup#getTilesetByIndex
+ * @public
+ * @function
+ * @param {Number} i
+ * @return {me.TMXTileset} corresponding tileset
+ */
+ getTilesetByIndex: function getTilesetByIndex(i) {
+ return this.tilesets[i];
+ },
+
+ /**
+ * return the tileset corresponding to the specified id
+ * will throw an exception if no matching tileset is found
+ * @name me.TMXTilesetGroup#getTilesetByGid
+ * @public
+ * @function
+ * @param {Number} gid
+ * @return {me.TMXTileset} corresponding tileset
+ */
+ getTilesetByGid: function getTilesetByGid(gid) {
+ var invalidRange = -1; // clear the gid of all flip/rotation flags
+
+ gid &= TMX_CLEAR_BIT_MASK; // cycle through all tilesets
+
+ for (var i = 0, len = this.tilesets.length; i < len; i++) {
+ // return the corresponding tileset if matching
+ if (this.tilesets[i].contains(gid)) {
+ return this.tilesets[i];
+ } // typically indicates a layer with no asset loaded (collision?)
+
+
+ if (this.tilesets[i].firstgid === this.tilesets[i].lastgid && gid >= this.tilesets[i].firstgid) {
+ // store the id if the [firstgid .. lastgid] is invalid
+ invalidRange = i;
+ }
+ } // return the tileset with the invalid range
+
+
+ if (invalidRange !== -1) {
+ return this.tilesets[invalidRange];
+ } else {
+ throw new Error("no matching tileset found for gid " + gid);
+ }
+ }
+ });
+ })();
+
+ (function () {
+ // scope global var & constants
+ var offsetsStaggerX = [{
+ x: 0,
+ y: 0
+ }, {
+ x: +1,
+ y: -1
+ }, {
+ x: +1,
+ y: 0
+ }, {
+ x: +2,
+ y: 0
+ }];
+ var offsetsStaggerY = [{
+ x: 0,
+ y: 0
+ }, {
+ x: -1,
+ y: +1
+ }, {
+ x: 0,
+ y: +1
+ }, {
+ x: 0,
+ y: +2
+ }];
+ /**
+ * The map renderer base class
+ * @class
+ * @extends me.Object
+ * @memberOf me
+ * @constructor
+ * @param {Number} cols width of the tilemap in tiles
+ * @param {Number} rows height of the tilemap in tiles
+ * @param {Number} tilewidth width of each tile in pixels
+ * @param {Number} tileheight height of each tile in pixels
+ */
+
+ me.TMXRenderer = me.Object.extend({
+ // constructor
+ init: function init(cols, rows, tilewidth, tileheight) {
+ this.cols = cols;
+ this.rows = rows;
+ this.tilewidth = tilewidth;
+ this.tileheight = tileheight;
+ },
+
+ /**
+ * return true if the renderer can render the specified layer
+ * @name me.TMXRenderer#canRender
+ * @public
+ * @function
+ * @param {me.TMXTileMap|me.TMXLayer} component TMX Map or Layer
+ * @return {boolean}
+ */
+ canRender: function canRender(component) {
+ return this.cols === component.cols && this.rows === component.rows && this.tilewidth === component.tilewidth && this.tileheight === component.tileheight;
+ },
+
+ /**
+ * return the tile position corresponding to the specified pixel
+ * @name me.TMXRenderer#pixelToTileCoords
+ * @public
+ * @function
+ * @param {Number} x X coordinate
+ * @param {Number} y Y coordinate
+ * @param {me.Vector2d} [vector] an optional vector object where to put the return values
+ * @return {me.Vector2d}
+ */
+ pixelToTileCoords: function pixelToTileCoords(x, y, v) {
+ return v;
+ },
+
+ /**
+ * return the pixel position corresponding of the specified tile
+ * @name me.TMXRenderer#tileToPixelCoords
+ * @public
+ * @function
+ * @param {Number} col tile horizontal position
+ * @param {Number} row tile vertical position
+ * @param {me.Vector2d} [vector] an optional vector object where to put the return values
+ * @return {me.Vector2d}
+ */
+ tileToPixelCoords: function tileToPixelCoords(x, y, v) {
+ return v;
+ },
+
+ /**
+ * draw the given tile at the specified layer
+ * @name me.TMXRenderer#drawTile
+ * @public
+ * @function
+ * @param {me.CanvasRenderer|me.WebGLRenderer} renderer a renderer object
+ * @param {Number} x X coordinate where to draw the tile
+ * @param {Number} y Y coordinate where to draw the tile
+ * @param {me.Tile} tile the tile object to draw
+ */
+ drawTile: function drawTile(renderer, x, y, tile) {},
+
+ /**
+ * draw the given TMX Layer for the given area
+ * @name me.TMXRenderer#drawTileLayer
+ * @public
+ * @function
+ * @param {me.CanvasRenderer|me.WebGLRenderer} renderer a renderer object
+ * @param {me.TMXLayer} layer a TMX Layer object
+ * @param {me.Rect} rect the area of the layer to draw
+ */
+ drawTileLayer: function drawTileLayer(renderer, layer, rect) {}
+ });
+ /**
+ * an Orthogonal Map Renderder
+ * @memberOf me
+ * @extends me.TMXRenderer
+ * @memberOf me
+ * @constructor
+ * @param {Number} cols width of the tilemap in tiles
+ * @param {Number} rows height of the tilemap in tiles
+ * @param {Number} tilewidth width of each tile in pixels
+ * @param {Number} tileheight height of each tile in pixels
+ */
+
+ me.TMXOrthogonalRenderer = me.TMXRenderer.extend({
+ /**
+ * return true if the renderer can render the specified layer
+ * @ignore
+ */
+ canRender: function canRender(layer) {
+ return layer.orientation === "orthogonal" && this._super(me.TMXRenderer, "canRender", [layer]);
+ },
+
+ /**
+ * return the tile position corresponding to the specified pixel
+ * @ignore
+ */
+ pixelToTileCoords: function pixelToTileCoords(x, y, v) {
+ var ret = v || new me.Vector2d();
+ return ret.set(x / this.tilewidth, y / this.tileheight);
+ },
+
+ /**
+ * return the pixel position corresponding of the specified tile
+ * @ignore
+ */
+ tileToPixelCoords: function tileToPixelCoords(x, y, v) {
+ var ret = v || new me.Vector2d();
+ return ret.set(x * this.tilewidth, y * this.tileheight);
+ },
+
+ /**
+ * fix the position of Objects to match
+ * the way Tiled places them
+ * @ignore
+ */
+ adjustPosition: function adjustPosition(obj) {
+ // only adjust position if obj.gid is defined
+ if (typeof obj.gid === "number") {
+ // Tiled objects origin point is "bottom-left" in Tiled,
+ // "top-left" in melonJS)
+ obj.y -= obj.height;
+ }
+ },
+
+ /**
+ * draw the tile map
+ * @ignore
+ */
+ drawTile: function drawTile(renderer, x, y, tmxTile) {
+ var tileset = tmxTile.tileset; // draw the tile
+
+ tileset.drawTile(renderer, tileset.tileoffset.x + x * this.tilewidth, tileset.tileoffset.y + (y + 1) * this.tileheight - tileset.tileheight, tmxTile);
+ },
+
+ /**
+ * draw the tile map
+ * @ignore
+ */
+ drawTileLayer: function drawTileLayer(renderer, layer, rect) {
+ var incX = 1,
+ incY = 1; // get top-left and bottom-right tile position
+
+ var start = this.pixelToTileCoords(Math.max(rect.pos.x - (layer.maxTileSize.width - layer.tilewidth), 0), Math.max(rect.pos.y - (layer.maxTileSize.height - layer.tileheight), 0), me.pool.pull("me.Vector2d")).floorSelf();
+ var end = this.pixelToTileCoords(rect.pos.x + rect.width + this.tilewidth, rect.pos.y + rect.height + this.tileheight, me.pool.pull("me.Vector2d")).ceilSelf(); //ensure we are in the valid tile range
+
+ end.x = end.x > this.cols ? this.cols : end.x;
+ end.y = end.y > this.rows ? this.rows : end.y;
+
+ switch (layer.renderorder) {
+ case "right-up":
+ // swapping start.y and end.y
+ end.y = start.y + (start.y = end.y) - end.y;
+ incY = -1;
+ break;
+
+ case "left-down":
+ // swapping start.x and end.x
+ end.x = start.x + (start.x = end.x) - end.x;
+ incX = -1;
+ break;
+
+ case "left-up":
+ // swapping start.x and end.x
+ end.x = start.x + (start.x = end.x) - end.x; // swapping start.y and end.y
+
+ end.y = start.y + (start.y = end.y) - end.y;
+ incX = -1;
+ incY = -1;
+ break;
+
+ default:
+ // right-down
+ break;
+ } // main drawing loop
+
+
+ for (var y = start.y; y !== end.y; y += incY) {
+ for (var x = start.x; x !== end.x; x += incX) {
+ var tmxTile = layer.layerData[x][y];
+
+ if (tmxTile) {
+ this.drawTile(renderer, x, y, tmxTile);
+ }
+ }
+ }
+
+ me.pool.push(start);
+ me.pool.push(end);
+ }
+ });
+ /**
+ * an Isometric Map Renderder
+ * @memberOf me
+ * @extends me.TMXRenderer
+ * @memberOf me
+ * @constructor
+ * @param {Number} cols width of the tilemap in tiles
+ * @param {Number} rows height of the tilemap in tiles
+ * @param {Number} tilewidth width of each tile in pixels
+ * @param {Number} tileheight height of each tile in pixels
+ */
+
+ me.TMXIsometricRenderer = me.TMXRenderer.extend({
+ // constructor
+ init: function init(cols, rows, tilewidth, tileheight) {
+ this._super(me.TMXRenderer, "init", [cols, rows, tilewidth, tileheight]);
+
+ this.hTilewidth = tilewidth / 2;
+ this.hTileheight = tileheight / 2;
+ this.originX = this.rows * this.hTilewidth;
+ },
+
+ /**
+ * return true if the renderer can render the specified layer
+ * @ignore
+ */
+ canRender: function canRender(layer) {
+ return layer.orientation === "isometric" && this._super(me.TMXRenderer, "canRender", [layer]);
+ },
+
+ /**
+ * return the tile position corresponding to the specified pixel
+ * @ignore
+ */
+ pixelToTileCoords: function pixelToTileCoords(x, y, v) {
+ var ret = v || new me.Vector2d();
+ return ret.set(y / this.tileheight + (x - this.originX) / this.tilewidth, y / this.tileheight - (x - this.originX) / this.tilewidth);
+ },
+
+ /**
+ * return the pixel position corresponding of the specified tile
+ * @ignore
+ */
+ tileToPixelCoords: function tileToPixelCoords(x, y, v) {
+ var ret = v || new me.Vector2d();
+ return ret.set((x - y) * this.hTilewidth + this.originX, (x + y) * this.hTileheight);
+ },
+
+ /**
+ * fix the position of Objects to match
+ * the way Tiled places them
+ * @ignore
+ */
+ adjustPosition: function adjustPosition(obj) {
+ var tileX = obj.x / this.hTilewidth;
+ var tileY = obj.y / this.tileheight;
+ var isoPos = me.pool.pull("me.Vector2d");
+ this.tileToPixelCoords(tileX, tileY, isoPos);
+ obj.x = isoPos.x;
+ obj.y = isoPos.y;
+ me.pool.push(isoPos);
+ },
+
+ /**
+ * draw the tile map
+ * @ignore
+ */
+ drawTile: function drawTile(renderer, x, y, tmxTile) {
+ var tileset = tmxTile.tileset; // draw the tile
+
+ tileset.drawTile(renderer, (this.cols - 1) * tileset.tilewidth + (x - y) * tileset.tilewidth >> 1, -tileset.tilewidth + (x + y) * tileset.tileheight >> 2, tmxTile);
+ },
+
+ /**
+ * draw the tile map
+ * @ignore
+ */
+ drawTileLayer: function drawTileLayer(renderer, layer, rect) {
+ // cache a couple of useful references
+ var tileset = layer.tileset; // get top-left and bottom-right tile position
+
+ var rowItr = this.pixelToTileCoords(rect.pos.x - tileset.tilewidth, rect.pos.y - tileset.tileheight, me.pool.pull("me.Vector2d")).floorSelf();
+ var tileEnd = this.pixelToTileCoords(rect.pos.x + rect.width + tileset.tilewidth, rect.pos.y + rect.height + tileset.tileheight, me.pool.pull("me.Vector2d")).ceilSelf();
+ var rectEnd = this.tileToPixelCoords(tileEnd.x, tileEnd.y, me.pool.pull("me.Vector2d")); // Determine the tile and pixel coordinates to start at
+
+ var startPos = this.tileToPixelCoords(rowItr.x, rowItr.y, me.pool.pull("me.Vector2d"));
+ startPos.x -= this.hTilewidth;
+ startPos.y += this.tileheight;
+ /* Determine in which half of the tile the top-left corner of the area we
+ * need to draw is. If we're in the upper half, we need to start one row
+ * up due to those tiles being visible as well. How we go up one row
+ * depends on whether we're in the left or right half of the tile.
+ */
+
+ var inUpperHalf = startPos.y - rect.pos.y > this.hTileheight;
+ var inLeftHalf = rect.pos.x - startPos.x < this.hTilewidth;
+
+ if (inUpperHalf) {
+ if (inLeftHalf) {
+ rowItr.x--;
+ startPos.x -= this.hTilewidth;
+ } else {
+ rowItr.y--;
+ startPos.x += this.hTilewidth;
+ }
+
+ startPos.y -= this.hTileheight;
+ } // Determine whether the current row is shifted half a tile to the right
+
+
+ var shifted = inUpperHalf ^ inLeftHalf; // initialize the columItr vector
+
+ var columnItr = rowItr.clone(); // main drawing loop
+
+ for (var y = startPos.y * 2; y - this.tileheight * 2 < rectEnd.y * 2; y += this.tileheight) {
+ columnItr.setV(rowItr);
+
+ for (var x = startPos.x; x < rectEnd.x; x += this.tilewidth) {
+ //check if it's valid tile, if so render
+ if (columnItr.x >= 0 && columnItr.y >= 0 && columnItr.x < this.cols && columnItr.y < this.rows) {
+ var tmxTile = layer.layerData[columnItr.x][columnItr.y];
+
+ if (tmxTile) {
+ tileset = tmxTile.tileset; // offset could be different per tileset
+
+ var offset = tileset.tileoffset; // draw our tile
+
+ tileset.drawTile(renderer, offset.x + x, offset.y + y / 2 - tileset.tileheight, tmxTile);
+ }
+ } // Advance to the next column
+
+
+ columnItr.x++;
+ columnItr.y--;
+ } // Advance to the next row
+
+
+ if (!shifted) {
+ rowItr.x++;
+ startPos.x += this.hTilewidth;
+ shifted = true;
+ } else {
+ rowItr.y++;
+ startPos.x -= this.hTilewidth;
+ shifted = false;
+ }
+ }
+
+ me.pool.push(columnItr);
+ me.pool.push(rowItr);
+ me.pool.push(tileEnd);
+ me.pool.push(rectEnd);
+ me.pool.push(startPos);
+ }
+ });
+ /**
+ * an Hexagonal Map Renderder
+ * @memberOf me
+ * @extends me.TMXRenderer
+ * @memberOf me
+ * @constructor
+ * @param {Number} cols width of the tilemap in tiles
+ * @param {Number} rows height of the tilemap in tiles
+ * @param {Number} tilewidth width of each tile in pixels
+ * @param {Number} tileheight height of each tile in pixels
+ */
+
+ me.TMXHexagonalRenderer = me.TMXRenderer.extend({
+ // constructor
+ init: function init(cols, rows, tilewidth, tileheight, hexsidelength, staggeraxis, staggerindex) {
+ this._super(me.TMXRenderer, "init", [cols, rows, tilewidth, tileheight]);
+
+ this.hexsidelength = hexsidelength;
+ this.staggeraxis = staggeraxis;
+ this.staggerindex = staggerindex;
+ this.sidelengthx = 0;
+ this.sidelengthy = 0;
+
+ if (staggeraxis === "x") {
+ this.sidelengthx = hexsidelength;
+ } else {
+ this.sidelengthy = hexsidelength;
+ }
+
+ this.sideoffsetx = (this.tilewidth - this.sidelengthx) / 2;
+ this.sideoffsety = (this.tileheight - this.sidelengthy) / 2;
+ this.columnwidth = this.sideoffsetx + this.sidelengthx;
+ this.rowheight = this.sideoffsety + this.sidelengthy;
+ this.centers = [new me.Vector2d(), new me.Vector2d(), new me.Vector2d(), new me.Vector2d()];
+ },
+
+ /**
+ * return true if the renderer can render the specified layer
+ * @ignore
+ */
+ canRender: function canRender(layer) {
+ return layer.orientation === "hexagonal" && this._super(me.TMXRenderer, "canRender", [layer]);
+ },
+
+ /**
+ * return the tile position corresponding to the specified pixel
+ * @ignore
+ */
+ pixelToTileCoords: function pixelToTileCoords(x, y, v) {
+ var q, r;
+ var ret = v || new me.Vector2d();
+
+ if (this.staggeraxis === "x") {
+ //flat top
+ x = x - (this.staggerindex === "odd" ? this.sideoffsetx : this.tilewidth);
+ } else {
+ //pointy top
+ y = y - (this.staggerindex === "odd" ? this.sideoffsety : this.tileheight);
+ } // Start with the coordinates of a grid-aligned tile
+
+
+ var referencePoint = me.pool.pull("me.Vector2d", Math.floor(x / (this.columnwidth * 2)), Math.floor(y / (this.rowheight * 2))); // Relative x and y position on the base square of the grid-aligned tile
+
+ var rel = me.pool.pull("me.Vector2d", x - referencePoint.x * (this.columnwidth * 2), y - referencePoint.y * (this.rowheight * 2)); // Adjust the reference point to the correct tile coordinates
+
+ if (this.staggeraxis === "x") {
+ referencePoint.x = referencePoint.x * 2;
+
+ if (this.staggerindex === "even") {
+ ++referencePoint.x;
+ }
+ } else {
+ referencePoint.y = referencePoint.y * 2;
+
+ if (this.staggerindex === "even") {
+ ++referencePoint.y;
+ }
+ } // Determine the nearest hexagon tile by the distance to the center
+
+
+ var left, top, centerX, centerY;
+
+ if (this.staggeraxis === "x") {
+ left = this.sidelengthx / 2;
+ centerX = left + this.columnwidth;
+ centerY = this.tileheight / 2;
+ this.centers[0].set(left, centerY);
+ this.centers[1].set(centerX, centerY - this.rowheight);
+ this.centers[2].set(centerX, centerY + this.rowheight);
+ this.centers[3].set(centerX + this.columnwidth, centerY);
+ } else {
+ top = this.sidelengthy / 2;
+ centerX = this.tilewidth / 2;
+ centerY = top + this.rowheight;
+ this.centers[0].set(centerX, top);
+ this.centers[1].set(centerX - this.columnwidth, centerY);
+ this.centers[2].set(centerX + this.columnwidth, centerY);
+ this.centers[3].set(centerX, centerY + this.rowheight);
+ }
+
+ var nearest = 0;
+ var minDist = Number.MAX_VALUE;
+ var dc;
+
+ for (var i = 0; i < 4; ++i) {
+ dc = Math.pow(this.centers[i].x - rel.x, 2) + Math.pow(this.centers[i].y - rel.y, 2);
+
+ if (dc < minDist) {
+ minDist = dc;
+ nearest = i;
+ }
+ }
+
+ var offsets = this.staggeraxis === "x" ? offsetsStaggerX : offsetsStaggerY;
+ q = referencePoint.x + offsets[nearest].x;
+ r = referencePoint.y + offsets[nearest].y;
+ me.pool.push(referencePoint);
+ me.pool.push(rel);
+ return ret.set(q, r);
+ },
+
+ /**
+ * return the pixel position corresponding of the specified tile
+ * @ignore
+ */
+ tileToPixelCoords: function tileToPixelCoords(q, r, v) {
+ var x, y;
+ var ret = v || new me.Vector2d();
+
+ if (this.staggeraxis === "x") {
+ //flat top
+ x = q * this.columnwidth;
+
+ if (this.staggerindex === "odd") {
+ y = r * (this.tileheight + this.sidelengthy);
+ y = y + this.rowheight * (q & 1);
+ } else {
+ y = r * (this.tileheight + this.sidelengthy);
+ y = y + this.rowheight * (1 - (q & 1));
+ }
+ } else {
+ //pointy top
+ y = r * this.rowheight;
+
+ if (this.staggerindex === "odd") {
+ x = q * (this.tilewidth + this.sidelengthx);
+ x = x + this.columnwidth * (r & 1);
+ } else {
+ x = q * (this.tilewidth + this.sidelengthx);
+ x = x + this.columnwidth * (1 - (r & 1));
+ }
+ }
+
+ return ret.set(x, y);
+ },
+
+ /**
+ * fix the position of Objects to match
+ * the way Tiled places them
+ * @ignore
+ */
+ adjustPosition: function adjustPosition(obj) {
+ // only adjust position if obj.gid is defined
+ if (typeof obj.gid === "number") {
+ // Tiled objects origin point is "bottom-left" in Tiled,
+ // "top-left" in melonJS)
+ obj.y -= obj.height;
+ }
+ },
+
+ /**
+ * draw the tile map
+ * @ignore
+ */
+ drawTile: function drawTile(renderer, x, y, tmxTile) {
+ var tileset = tmxTile.tileset;
+ var point = this.tileToPixelCoords(x, y, me.pool.pull("me.Vector2d")); // draw the tile
+
+ tileset.drawTile(renderer, tileset.tileoffset.x + point.x, tileset.tileoffset.y + point.y + (this.tileheight - tileset.tileheight), tmxTile);
+ me.pool.push(point);
+ },
+
+ /**
+ * draw the tile map
+ * @ignore
+ */
+ drawTileLayer: function drawTileLayer(renderer, layer, rect) {
+ // get top-left and bottom-right tile position
+ var start = this.pixelToTileCoords(rect.pos.x, rect.pos.y, me.pool.pull("me.Vector2d")).floorSelf();
+ var end = this.pixelToTileCoords(rect.pos.x + rect.width + this.tilewidth, rect.pos.y + rect.height + this.tileheight, me.pool.pull("me.Vector2d")).ceilSelf(); //ensure we are in the valid tile range
+
+ start.x = start.x < 0 ? 0 : start.x;
+ start.y = start.y < 0 ? 0 : start.y;
+ end.x = end.x > this.cols ? this.cols : end.x;
+ end.y = end.y > this.rows ? this.rows : end.y; // main drawing loop
+
+ for (var y = start.y; y < end.y; y++) {
+ for (var x = start.x; x < end.x; x++) {
+ var tmxTile = layer.layerData[x][y];
+
+ if (tmxTile) {
+ this.drawTile(renderer, x, y, tmxTile);
+ }
+ }
+ }
+
+ me.pool.push(start);
+ me.pool.push(end);
+ }
+ });
+ })();
+
+ (function () {
+ /**
+ * Create required arrays for the given layer object
+ * @ignore
+ */
+ function initArray(layer) {
+ // initialize the array
+ layer.layerData = new Array(layer.cols);
+
+ for (var x = 0; x < layer.cols; x++) {
+ layer.layerData[x] = new Array(layer.rows);
+
+ for (var y = 0; y < layer.rows; y++) {
+ layer.layerData[x][y] = null;
+ }
+ }
+ }
+ /**
+ * Set a tiled layer Data
+ * @ignore
+ */
+
+
+ function setLayerData(layer, data) {
+ var idx = 0; // initialize the array
+
+ initArray(layer); // set everything
+
+ for (var y = 0; y < layer.rows; y++) {
+ for (var x = 0; x < layer.cols; x++) {
+ // get the value of the gid
+ var gid = data[idx++]; // fill the array
+
+ if (gid !== 0) {
+ // add a new tile to the layer
+ layer.setTile(x, y, gid);
+ }
+ }
+ }
+ }
+ /**
+ * a TMX Tile Layer Object
+ * Tiled QT 0.7.x format
+ * @class
+ * @extends me.Renderable
+ * @memberOf me
+ * @constructor
+ * @param {Object} data layer data in JSON format ({@link http://docs.mapeditor.org/en/stable/reference/tmx-map-format/#layer})
+ * @param {Number} tilewidth width of each tile in pixels
+ * @param {Number} tileheight height of each tile in pixels
+ * @param {String} orientation "isometric" or "orthogonal"
+ * @param {me.TMXTilesetGroup} tilesets tileset as defined in Tiled
+ * @param {Number} z z-index position
+ */
+
+
+ me.TMXLayer = me.Renderable.extend({
+ /**
+ * @ignore
+ */
+ init: function init(data, tilewidth, tileheight, orientation, tilesets, z) {
+ // super constructor
+ this._super(me.Renderable, "init", [0, 0, 0, 0]); // tile width & height
+
+
+ this.tilewidth = data.tilewidth || tilewidth;
+ this.tileheight = data.tileheight || tileheight; // layer orientation
+
+ this.orientation = orientation;
+ /**
+ * The Layer corresponding Tilesets
+ * @public
+ * @type me.TMXTilesetGroup
+ * @name me.TMXLayer#tilesets
+ */
+
+ this.tilesets = tilesets; // the default tileset
+ // XXX: Is this even used?
+
+ this.tileset = this.tilesets ? this.tilesets.getTilesetByIndex(0) : null; // Biggest tile size to draw
+
+ this.maxTileSize = {
+ "width": 0,
+ "height": 0
+ };
+
+ for (var i = 0; i < this.tilesets.length; i++) {
+ var tileset = this.tilesets.getTilesetByIndex(i);
+ this.maxTileSize.width = Math.max(this.maxTileSize.width, tileset.tilewidth);
+ this.maxTileSize.height = Math.max(this.maxTileSize.height, tileset.tileheight);
+ }
+ /**
+ * All animated tilesets in this layer
+ * @ignore
+ * @type Array
+ * @name me.TMXLayer#animatedTilesets
+ */
+
+
+ this.animatedTilesets = [];
+ /**
+ * Layer contains tileset animations
+ * @public
+ * @type Boolean
+ * @name me.TMXLayer#isAnimated
+ */
+
+ this.isAnimated = false;
+ /**
+ * the order in which tiles on orthogonal tile layers are rendered.
+ * (valid values are "left-down", "left-up", "right-down", "right-up")
+ * @public
+ * @type {String}
+ * @default "right-down"
+ * @name me.TMXLayer#renderorder
+ */
+
+ this.renderorder = data.renderorder || "right-down"; // for displaying order
+
+ this.pos.z = z; // tiled default coordinates are top-left
+
+ this.anchorPoint.set(0, 0); // additional TMX flags
+
+ this.name = data.name;
+ this.cols = +data.width;
+ this.rows = +data.height; // hexagonal maps only
+
+ this.hexsidelength = +data.hexsidelength || undefined;
+ this.staggeraxis = data.staggeraxis;
+ this.staggerindex = data.staggerindex; // layer opacity
+
+ var visible = typeof data.visible !== "undefined" ? +data.visible : 1;
+ this.setOpacity(visible ? +data.opacity : 0); // layer "real" size
+
+ if (this.orientation === "isometric") {
+ this.width = (this.cols + this.rows) * (this.tilewidth / 2);
+ this.height = (this.cols + this.rows) * (this.tileheight / 2);
+ } else {
+ this.width = this.cols * this.tilewidth;
+ this.height = this.rows * this.tileheight;
+ } // check if we have any user-defined properties
+
+
+ me.TMXUtils.applyTMXProperties(this, data); // check for the correct rendering method
+
+ if (typeof this.preRender === "undefined") {
+ this.preRender = me.sys.preRender;
+ } // if pre-rendering method is use, create an offline canvas/renderer
+
+
+ if (this.preRender === true) {
+ this.canvasRenderer = new me.CanvasRenderer(me.video.createCanvas(this.width, this.height), this.width, this.height, {
+ transparent: true
+ });
+ } // initialize and set the layer data
+
+
+ setLayerData(this, me.TMXUtils.decode(data.data, data.encoding, data.compression));
+ },
+ // called when the layer is added to the game world or a container
+ onActivateEvent: function onActivateEvent() {
+ if (this.animatedTilesets === undefined) {
+ this.animatedTilesets = [];
+ }
+
+ if (this.tilesets) {
+ var tileset = this.tilesets.tilesets;
+
+ for (var i = 0; i < tileset.length; i++) {
+ if (tileset[i].isAnimated) {
+ this.animatedTilesets.push(tileset[i]);
+ }
+ }
+ }
+
+ this.isAnimated = this.animatedTilesets.length > 0; // Force pre-render off when tileset animation is used
+
+ if (this.isAnimated) {
+ this.preRender = false;
+ } // Resize the bounding rect
+
+
+ this.getBounds().resize(this.width, this.height);
+ },
+ // called when the layer is removed from the game world or a container
+ onDeactivateEvent: function onDeactivateEvent() {
+ // clear all allocated objects
+ //this.layerData = undefined;
+ this.animatedTilesets = undefined;
+ },
+
+ /**
+ * Se the TMX renderer for this layer object
+ * @name setRenderer
+ * @memberOf me.TMXLayer
+ * @public
+ * @function
+ * @param {me.TMXRenderer} renderer
+ */
+ setRenderer: function setRenderer(renderer) {
+ this.renderer = renderer;
+ },
+
+ /**
+ * Return the layer current renderer object
+ * @name getRenderer
+ * @memberOf me.TMXLayer
+ * @public
+ * @function
+ * @return {me.TMXRenderer} renderer
+ */
+ getRenderer: function getRenderer(renderer) {
+ return this.renderer;
+ },
+
+ /**
+ * Return the TileId of the Tile at the specified position
+ * @name getTileId
+ * @memberOf me.TMXLayer
+ * @public
+ * @function
+ * @param {Number} x X coordinate (in world/pixels coordinates)
+ * @param {Number} y Y coordinate (in world/pixels coordinates)
+ * @return {Number} TileId or null if there is no Tile at the given position
+ */
+ getTileId: function getTileId(x, y) {
+ var tile = this.getTile(x, y);
+ return tile ? tile.tileId : null;
+ },
+
+ /**
+ * Return the Tile object at the specified position
+ * @name getTile
+ * @memberOf me.TMXLayer
+ * @public
+ * @function
+ * @param {Number} x X coordinate (in world/pixels coordinates)
+ * @param {Number} y Y coordinate (in world/pixels coordinates)
+ * @return {me.Tile} corresponding tile or null if outside of the map area
+ * @example
+ * // get the TMX Map Layer called "Front layer"
+ * var layer = me.game.world.getChildByName("Front Layer")[0];
+ * // get the tile object corresponding to the latest pointer position
+ * var tile = layer.getTile(me.input.pointer.pos.x, me.input.pointer.pos.y);
+ */
+ getTile: function getTile(x, y) {
+ if (this.containsPoint(x, y)) {
+ var renderer = this.renderer;
+ var tile = null;
+ var coord = renderer.pixelToTileCoords(x, y, me.pool.pull("me.Vector2d"));
+
+ if (coord.x >= 0 && coord.x < renderer.cols && coord.y >= 0 && coord.y < renderer.rows) {
+ tile = this.layerData[~~coord.x][~~coord.y];
+ }
+
+ me.pool.push(coord);
+ }
+
+ return tile;
+ },
+
+ /**
+ * Create a new Tile at the specified position
+ * @name setTile
+ * @memberOf me.TMXLayer
+ * @public
+ * @function
+ * @param {Number} x X coordinate (in map coordinates: row/column)
+ * @param {Number} y Y coordinate (in map coordinates: row/column)
+ * @param {Number} tileId tileId
+ * @return {me.Tile} the corresponding newly created tile object
+ */
+ setTile: function setTile(x, y, tileId) {
+ if (!this.tileset.contains(tileId)) {
+ // look for the corresponding tileset
+ this.tileset = this.tilesets.getTilesetByGid(tileId);
+ }
+
+ var tile = this.layerData[x][y] = new me.Tile(x, y, tileId, this.tileset); // draw the corresponding tile
+
+ if (this.preRender) {
+ this.renderer.drawTile(this.canvasRenderer, x, y, tile);
+ }
+
+ return tile;
+ },
+
+ /**
+ * clear the tile at the specified position
+ * @name clearTile
+ * @memberOf me.TMXLayer
+ * @public
+ * @function
+ * @param {Number} x X coordinate (in map coordinates: row/column)
+ * @param {Number} y Y coordinate (in map coordinates: row/column)
+ * @example
+ * me.game.world.getChildByType(me.TMXLayer).forEach(function(layer) {
+ * // clear all tiles at the given x,y coordinates
+ * layer.clearTile(x, y);
+ * });
+ */
+ clearTile: function clearTile(x, y) {
+ // clearing tile
+ this.layerData[x][y] = null; // erase the corresponding area in the canvas
+
+ if (this.preRender) {
+ this.canvasRenderer.clearRect(x * this.tilewidth, y * this.tileheight, this.tilewidth, this.tileheight);
+ }
+ },
+
+ /**
+ * update animations in a tileset layer
+ * @ignore
+ */
+ update: function update(dt) {
+ if (this.isAnimated) {
+ var result = false;
+
+ for (var i = 0; i < this.animatedTilesets.length; i++) {
+ result = this.animatedTilesets[i].update(dt) || result;
+ }
+
+ return result;
+ }
+
+ return false;
+ },
+
+ /**
+ * draw a tileset layer
+ * @ignore
+ */
+ draw: function draw(renderer, rect) {
+ // use the offscreen canvas
+ if (this.preRender) {
+ var width = Math.min(rect.width, this.width);
+ var height = Math.min(rect.height, this.height); // draw using the cached canvas
+
+ renderer.drawImage(this.canvasRenderer.getCanvas(), rect.pos.x, rect.pos.y, // sx,sy
+ width, height, // sw,sh
+ rect.pos.x, rect.pos.y, // dx,dy
+ width, height // dw,dh
+ );
+ } // dynamically render the layer
+ else {
+ // draw the layer
+ this.renderer.drawTileLayer(renderer, this, rect);
+ }
+ }
+ });
+ })();
+
+ (function () {
+ // constant to identify the collision object layer
+ var COLLISION_GROUP = "collision";
+ /**
+ * set a compatible renderer object
+ * for the specified map
+ * @ignore
+ */
+
+ function getNewDefaultRenderer(obj) {
+ switch (obj.orientation) {
+ case "orthogonal":
+ return new me.TMXOrthogonalRenderer(obj.cols, obj.rows, obj.tilewidth, obj.tileheight);
+
+ case "isometric":
+ return new me.TMXIsometricRenderer(obj.cols, obj.rows, obj.tilewidth, obj.tileheight);
+
+ case "hexagonal":
+ return new me.TMXHexagonalRenderer(obj.cols, obj.rows, obj.tilewidth, obj.tileheight, obj.hexsidelength, obj.staggeraxis, obj.staggerindex);
+ // if none found, throw an exception
+
+ default:
+ throw new Error(obj.orientation + " type TMX Tile Map not supported!");
+ }
+ }
+ /**
+ * read the layer Data
+ * @ignore
+ */
+
+
+ function readLayer(map, data, z) {
+ var layer = new me.TMXLayer(data, map.tilewidth, map.tileheight, map.orientation, map.tilesets, z); // set a renderer
+
+ layer.setRenderer(map.getRenderer(layer));
+ return layer;
+ }
+ /**
+ * read the Image Layer Data
+ * @ignore
+ */
+
+
+ function readImageLayer(map, data, z) {
+ // Normalize properties
+ me.TMXUtils.applyTMXProperties(data.properties, data); // create the layer
+
+ var imageLayer = me.pool.pull("me.ImageLayer", +data.x || 0, +data.y || 0, Object.assign({
+ name: data.name,
+ image: data.image,
+ z: z
+ }, data.properties)); // set some additional flags
+
+ var visible = typeof data.visible !== "undefined" ? data.visible : true;
+ imageLayer.setOpacity(visible ? +data.opacity : 0);
+ return imageLayer;
+ }
+ /**
+ * read the tileset Data
+ * @ignore
+ */
+
+
+ function readTileset(data) {
+ return new me.TMXTileset(data);
+ }
+ /**
+ * read the object group Data
+ * @ignore
+ */
+
+
+ function readObjectGroup(map, data, z) {
+ return new me.TMXGroup(map, data, z);
+ }
+ /**
+ * a TMX Tile Map Object
+ * Tiled QT +0.7.x format
+ * @class
+ * @extends me.Object
+ * @memberOf me
+ * @constructor
+ * @param {String} levelId name of TMX map
+ * @param {Object} data TMX map in JSON format
+ * @example
+ * // create a new level object based on the TMX JSON object
+ * var level = new me.TMXTileMap(levelId, me.loader.getTMX(levelId));
+ * // add the level to the game world container
+ * level.addTo(me.game.world, true);
+ */
+
+
+ me.TMXTileMap = me.Object.extend({
+ // constructor
+ init: function init(levelId, data) {
+ /**
+ * the level data (JSON)
+ * @ignore
+ */
+ this.data = data;
+ /**
+ * name of the tilemap
+ * @public
+ * @type {String}
+ * @name me.TMXTileMap#name
+ */
+
+ this.name = levelId;
+ /**
+ * width of the tilemap in tiles
+ * @public
+ * @type {Number}
+ * @name me.TMXTileMap#cols
+ */
+
+ this.cols = +data.width;
+ /**
+ * height of the tilemap in tiles
+ * @public
+ * @type {Number}
+ * @name me.TMXTileMap#rows
+ */
+
+ this.rows = +data.height;
+ /**
+ * Tile width
+ * @public
+ * @type {Number}
+ * @name me.TMXTileMap#tilewidth
+ */
+
+ this.tilewidth = +data.tilewidth;
+ /**
+ * Tile height
+ * @public
+ * @type {Number}
+ * @name me.TMXTileMap#tileheight
+ */
+
+ this.tileheight = +data.tileheight;
+ /**
+ * is the map an infinite map
+ * @public
+ * @type {Number}
+ * @default 0
+ * @name me.TMXTileMap#infinite
+ */
+
+ this.infinite = +data.infinite;
+ /**
+ * the map orientation type. melonJS supports “orthogonal”, “isometric”, “staggered” and “hexagonal”.
+ * @public
+ * @type {String}
+ * @default "orthogonal"
+ * @name me.TMXTileMap#orientation
+ */
+
+ this.orientation = data.orientation;
+ /**
+ * the order in which tiles on orthogonal tile layers are rendered.
+ * (valid values are "left-down", "left-up", "right-down", "right-up")
+ * @public
+ * @type {String}
+ * @default "right-down"
+ * @name me.TMXTileMap#renderorder
+ */
+
+ this.renderorder = data.renderorder || "right-down";
+ /**
+ * the TMX format version
+ * @public
+ * @type {String}
+ * @name me.TMXTileMap#version
+ */
+
+ this.version = data.version;
+ /**
+ * The Tiled version used to save the file (since Tiled 1.0.1).
+ * @public
+ * @type {String}
+ * @name me.TMXTileMap#tiledversion
+ */
+
+ this.tiledversion = data.tiledversion; // tilesets for this map
+
+ this.tilesets = null; // layers
+
+ if (typeof this.layers === "undefined") {
+ this.layers = [];
+ } // group objects
+
+
+ if (typeof this.objectGroups === "undefined") {
+ this.objectGroups = [];
+ } // Check if map is from melon editor
+
+
+ this.isEditor = data.editor === "melon-editor";
+
+ if (this.orientation === "isometric") {
+ this.width = (this.cols + this.rows) * (this.tilewidth / 2);
+ this.height = (this.cols + this.rows) * (this.tileheight / 2);
+ } else {
+ this.width = this.cols * this.tilewidth;
+ this.height = this.rows * this.tileheight;
+ } // object id
+
+
+ this.nextobjectid = +data.nextobjectid || undefined; // hex/iso properties
+
+ this.hexsidelength = +data.hexsidelength || undefined;
+ this.staggeraxis = data.staggeraxis;
+ this.staggerindex = data.staggerindex; // background color
+
+ this.backgroundcolor = data.backgroundcolor; // set additional map properties (if any)
+
+ me.TMXUtils.applyTMXProperties(this, data); // internal flag
+
+ this.initialized = false;
+
+ if (this.infinite === 1) {
+ // #956 Support for Infinite map
+ // see as well in me.TMXUtils
+ throw new Error("Tiled Infinite Map not supported!");
+ }
+ },
+
+ /**
+ * Return the map default renderer
+ * @name getRenderer
+ * @memberOf me.TMXTileMap
+ * @public
+ * @function
+ * @param {me.TMXLayer} [layer] a layer object
+ * @return {me.TMXRenderer} a TMX renderer
+ */
+ getRenderer: function getRenderer(layer) {
+ // first ensure a renderer is associated to this map
+ if (typeof this.renderer === "undefined" || !this.renderer.canRender(this)) {
+ this.renderer = getNewDefaultRenderer(this);
+ } // return a renderer for the given layer (if any)
+
+
+ if (typeof layer !== "undefined" && !this.renderer.canRender(layer)) {
+ return getNewDefaultRenderer(layer);
+ } // else return this renderer
+
+
+ return this.renderer;
+ },
+
+ /**
+ * parse the map
+ * @ignore
+ */
+ readMapObjects: function readMapObjects(data) {
+ if (this.initialized === true) {
+ return;
+ } // to automatically increment z index
+
+
+ var zOrder = 0;
+ var self = this; // Tileset information
+
+ if (!this.tilesets) {
+ // make sure we have a TilesetGroup Object
+ this.tilesets = new me.TMXTilesetGroup();
+ } // parse all tileset objects
+
+
+ if (typeof data.tilesets !== "undefined") {
+ var tilesets = data.tilesets;
+ tilesets.forEach(function (tileset) {
+ // add the new tileset
+ self.tilesets.add(readTileset(tileset));
+ });
+ } // check if a user-defined background color is defined
+
+
+ if (this.backgroundcolor) {
+ this.layers.push(me.pool.pull("me.ColorLayer", "background_color", this.backgroundcolor, zOrder++));
+ } // check if a background image is defined
+
+
+ if (this.background_image) {
+ // add a new image layer
+ this.layers.push(me.pool.pull("me.ImageLayer", 0, 0, {
+ name: "background_image",
+ image: this.background_image,
+ z: zOrder++
+ }));
+ }
+
+ data.layers.forEach(function (layer) {
+ switch (layer.type) {
+ case "imagelayer":
+ self.layers.push(readImageLayer(self, layer, zOrder++));
+ break;
+
+ case "tilelayer":
+ self.layers.push(readLayer(self, layer, zOrder++));
+ break;
+ // get the object groups information
+
+ case "objectgroup":
+ self.objectGroups.push(readObjectGroup(self, layer, zOrder++));
+ break;
+ // get the object groups information
+
+ case "group":
+ self.objectGroups.push(readObjectGroup(self, layer, zOrder++));
+ break;
+
+ default:
+ break;
+ }
+ });
+ this.initialized = true;
+ },
+
+ /**
+ * add all the map layers and objects to the given container
+ * @name me.TMXTileMap#addTo
+ * @public
+ * @function
+ * @param {me.Container} target container
+ * @param {boolean} flatten if true, flatten all objects into the given container
+ * @example
+ * // create a new level object based on the TMX JSON object
+ * var level = new me.TMXTileMap(levelId, me.loader.getTMX(levelId));
+ * // add the level to the game world container
+ * level.addTo(me.game.world, true);
+ */
+ addTo: function addTo(container, flatten) {
+ var _sort = container.autoSort;
+ var _depth = container.autoDepth; // disable auto-sort and auto-depth
+
+ container.autoSort = false;
+ container.autoDepth = false; // add all layers instances
+
+ this.getLayers().forEach(function (layer) {
+ container.addChild(layer);
+ }); // add all Object instances
+
+ this.getObjects(flatten).forEach(function (object) {
+ container.addChild(object);
+ }); // set back auto-sort and auto-depth
+
+ container.autoSort = _sort;
+ container.autoDepth = _depth; // force a sort
+
+ container.sort(true);
+ },
+
+ /**
+ * return an Array of instantiated objects, based on the map object definition
+ * @name me.TMXTileMap#getObjects
+ * @public
+ * @function
+ * @param {boolean} flatten if true, flatten all objects into the returned array,
+ * ignoring all defined groups (no sub containers will be created)
+ * @return {me.Renderable[]} Array of Objects
+ */
+ getObjects: function getObjects(flatten) {
+ var objects = [];
+ var isCollisionGroup = false;
+ var targetContainer; // parse the map for objects
+
+ this.readMapObjects(this.data);
+
+ for (var g = 0; g < this.objectGroups.length; g++) {
+ var group = this.objectGroups[g]; // check if this is the collision shape group
+
+ isCollisionGroup = group.name.toLowerCase().includes(COLLISION_GROUP);
+
+ if (flatten === false) {
+ // create a new container
+ targetContainer = new me.Container(0, 0, this.width, this.height); // tiled uses 0,0 by default
+
+ targetContainer.anchorPoint.set(0, 0); // set additional properties
+
+ targetContainer.name = group.name;
+ targetContainer.pos.z = group.z;
+ targetContainer.setOpacity(group.opacity); // disable auto-sort and auto-depth
+
+ targetContainer.autoSort = false;
+ targetContainer.autoDepth = false;
+ } // iterate through the group and add all object into their
+ // corresponding target Container
+
+
+ for (var o = 0; o < group.objects.length; o++) {
+ // TMX object settings
+ var settings = group.objects[o]; // reference to the instantiated object
+
+ var obj; // Tiled uses 0,0 by default
+
+ if (typeof settings.anchorPoint === "undefined") {
+ settings.anchorPoint = {
+ x: 0,
+ y: 0
+ };
+ } // groups can contains either text, objects or layers
+
+
+ if (settings instanceof me.TMXLayer) {
+ // layers are already instantiated & initialized
+ obj = settings; // z value set already
+ } else if (_typeof(settings.text) === "object") {
+ // Tiled uses 0,0 by default
+ if (typeof settings.text.anchorPoint === "undefined") {
+ settings.text.anchorPoint = settings.anchorPoint;
+ }
+
+ if (settings.text.bitmap === true) {
+ obj = me.pool.pull("me.BitmapText", settings.x, settings.y, settings.text);
+ } else {
+ obj = me.pool.pull("me.Text", settings.x, settings.y, settings.text);
+ } // set the obj z order
+
+
+ obj.pos.z = settings.z;
+ } else {
+ // pull the corresponding entity from the object pool
+ obj = me.pool.pull(settings.name || "me.Entity", settings.x, settings.y, settings); // set the obj z order
+
+ obj.pos.z = settings.z;
+ } // check if a me.Tile object is embedded
+
+
+ if (_typeof(settings.tile) === "object" && !obj.renderable) {
+ obj.renderable = settings.tile.getRenderable(settings); // adjust position if necessary
+
+ switch (settings.rotation) {
+ case Math.PI:
+ obj.translate(-obj.renderable.width, obj.renderable.height);
+ break;
+
+ case Math.PI / 2:
+ obj.translate(0, obj.renderable.height);
+ break;
+
+ case -(Math.PI / 2):
+ obj.translate(-obj.renderable.width, 0);
+ break;
+
+ default:
+ // this should not happen
+ break;
+ } // tile object use use left-bottom coordinates
+ //obj.anchorPoint.set(0, 1);
+
+ }
+
+ if (isCollisionGroup && !settings.name) {
+ // configure the body accordingly
+ obj.body.collisionType = me.collision.types.WORLD_SHAPE;
+ } //apply group opacity value to the child objects if group are merged
+
+
+ if (flatten === true) {
+ if (obj.isRenderable === true) {
+ obj.setOpacity(obj.getOpacity() * group.opacity); // and to child renderables if any
+
+ if (obj.renderable instanceof me.Renderable) {
+ obj.renderable.setOpacity(obj.renderable.getOpacity() * group.opacity);
+ }
+ } // directly add the obj into the objects array
+
+
+ objects.push(obj);
+ } else
+ /* false*/
+ {
+ // add it to the new container
+ targetContainer.addChild(obj);
+ }
+ } // if we created a new container
+
+
+ if (flatten === false && targetContainer.children.length > 0) {
+ // re-enable auto-sort and auto-depth
+ targetContainer.autoSort = true;
+ targetContainer.autoDepth = true; // add our container to the world
+
+ objects.push(targetContainer);
+ }
+ }
+
+ return objects;
+ },
+
+ /**
+ * return all the existing layers
+ * @name me.TMXTileMap#getLayers
+ * @public
+ * @function
+ * @return {me.TMXLayer[]} Array of Layers
+ */
+ getLayers: function getLayers() {
+ // parse the map for objects
+ this.readMapObjects(this.data);
+ return this.layers;
+ },
+
+ /**
+ * destroy function, clean all allocated objects
+ * @name me.TMXTileMap#destroy
+ * @public
+ * @function
+ */
+ destroy: function destroy() {
+ this.tilesets = undefined;
+ this.layers.length = 0;
+ this.objectGroups.length = 0;
+ this.initialized = false;
+ }
+ });
+ })();
+
+ (function () {
+ /**
+ * a level manager object
+ * once ressources loaded, the level director contains all references of defined levels
+ * There is no constructor function for me.levelDirector, this is a static object
+ * @namespace me.levelDirector
+ * @memberOf me
+ */
+ me.levelDirector = function () {
+ // hold public stuff in our singletong
+ var api = {};
+ /*
+ * PRIVATE STUFF
+ */
+ // our levels
+
+ var levels = {}; // level index table
+
+ var levelIdx = []; // current level index
+
+ var currentLevelIdx = 0; // onresize handler
+
+ var onresize_handler = null;
+
+ function safeLoadLevel(levelId, options, restart) {
+ // clean the destination container
+ options.container.reset(); // reset the renderer
+
+ me.game.reset(); // clean the current (previous) level
+
+ if (levels[api.getCurrentLevelId()]) {
+ levels[api.getCurrentLevelId()].destroy();
+ } // update current level index
+
+
+ currentLevelIdx = levelIdx.indexOf(levelId); // add the specified level to the game world
+
+ loadTMXLevel(levelId, options.container, options.flatten, options.setViewportBounds); // publish the corresponding message
+
+ me.event.publish(me.event.LEVEL_LOADED, [levelId]); // fire the callback
+
+ options.onLoaded(levelId);
+
+ if (restart) {
+ // resume the game loop if it was previously running
+ me.state.restart();
+ }
+ }
+ /**
+ * Load a TMX level
+ * @name loadTMXLevel
+ * @memberOf me.game
+ * @private
+ * @param {String} level level id
+ * @param {me.Container} target container
+ * @param {boolean} flatten if true, flatten all objects into the given container
+ * @param {boolean} setViewportBounds if true, set the viewport bounds to the map size
+ * @ignore
+ * @function
+ */
+
+
+ function loadTMXLevel(levelId, container, flatten, setViewportBounds) {
+ var level = levels[levelId]; // disable auto-sort for the given container
+
+ var autoSort = container.autoSort;
+ container.autoSort = false;
+
+ if (setViewportBounds) {
+ // update the viewport bounds
+ me.game.viewport.setBounds(0, 0, Math.max(level.width, me.game.viewport.width), Math.max(level.height, me.game.viewport.height));
+ } // reset the GUID generator
+ // and pass the level id as parameter
+
+
+ me.utils.resetGUID(levelId, level.nextobjectid); // Tiled use 0,0 anchor coordinates
+
+ container.anchorPoint.set(0, 0); // add all level elements to the target container
+
+ level.addTo(container, flatten); // sort everything (recursively)
+
+ container.sort(true);
+ container.autoSort = autoSort;
+ container.resize(level.width, level.height);
+
+ function resize_container() {
+ // center the map if smaller than the current viewport
+ container.pos.set(Math.max(0, ~~((me.game.viewport.width - level.width) / 2)), Math.max(0, ~~((me.game.viewport.height - level.height) / 2)), 0);
+ }
+
+ if (setViewportBounds) {
+ resize_container(); // Replace the resize handler
+
+ if (onresize_handler) {
+ me.event.unsubscribe(onresize_handler);
+ }
+
+ onresize_handler = me.event.subscribe(me.event.VIEWPORT_ONRESIZE, resize_container);
+ }
+ }
+ /*
+ * PUBLIC STUFF
+ */
+
+ /**
+ * reset the level director
+ * @ignore
+ */
+
+
+ api.reset = function () {};
+ /**
+ * add a level
+ * @ignore
+ */
+
+
+ api.addLevel = function () {
+ throw new Error("no level loader defined");
+ };
+ /**
+ * add a TMX level
+ * @ignore
+ */
+
+
+ api.addTMXLevel = function (levelId, callback) {
+ // just load the level with the XML stuff
+ if (levels[levelId] == null) {
+ //console.log("loading "+ levelId);
+ levels[levelId] = new me.TMXTileMap(levelId, me.loader.getTMX(levelId)); // level index
+
+ levelIdx.push(levelId);
+ } else {
+ //console.log("level %s already loaded", levelId);
+ return false;
+ } // call the callback if defined
+
+
+ if (callback) {
+ callback();
+ } // true if level loaded
+
+
+ return true;
+ };
+ /**
+ * load a level into the game manager
+ * (will also create all level defined entities, etc..)
+ * @name loadLevel
+ * @memberOf me.levelDirector
+ * @public
+ * @function
+ * @param {String} level level id
+ * @param {Object} [options] additional optional parameters
+ * @param {me.Container} [options.container=me.game.world] container in which to load the specified level
+ * @param {function} [options.onLoaded=me.game.onLevelLoaded] callback for when the level is fully loaded
+ * @param {boolean} [options.flatten=me.game.mergeGroup] if true, flatten all objects into the given container
+ * @param {boolean} [options.setViewportBounds=true] if true, set the viewport bounds to the map size
+ * @example
+ * // the game assets to be be preloaded
+ * // TMX maps
+ * var resources = [
+ * {name: "a4_level1", type: "tmx", src: "data/level/a4_level1.tmx"},
+ * {name: "a4_level2", type: "tmx", src: "data/level/a4_level2.tmx"},
+ * {name: "a4_level3", type: "tmx", src: "data/level/a4_level3.tmx"},
+ * // ...
+ * ];
+ *
+ * // ...
+ *
+ * // load a level into the game world
+ * me.levelDirector.loadLevel("a4_level1");
+ * ...
+ * ...
+ * // load a level into a specific container
+ * var levelContainer = new me.Container();
+ * me.levelDirector.loadLevel("a4_level2", {container:levelContainer});
+ * // add a simple transformation
+ * levelContainer.currentTransform.translate(levelContainer.width / 2, levelContainer.height / 2 );
+ * levelContainer.currentTransform.rotate(0.05);
+ * levelContainer.currentTransform.translate(-levelContainer.width / 2, -levelContainer.height / 2 );
+ * // add it to the game world
+ * me.game.world.addChild(levelContainer);
+ */
+
+
+ api.loadLevel = function (levelId, options) {
+ options = Object.assign({
+ "container": me.game.world,
+ "onLoaded": me.game.onLevelLoaded,
+ "flatten": me.game.mergeGroup,
+ "setViewportBounds": true
+ }, options || {}); // throw an exception if not existing
+
+ if (typeof levels[levelId] === "undefined") {
+ throw new Error("level " + levelId + " not found");
+ }
+
+ if (levels[levelId] instanceof me.TMXTileMap) {
+ // check the status of the state mngr
+ var wasRunning = me.state.isRunning();
+
+ if (wasRunning) {
+ // stop the game loop to avoid
+ // some silly side effects
+ me.state.stop();
+ me.utils.function.defer(safeLoadLevel, this, levelId, options, true);
+ } else {
+ safeLoadLevel(levelId, options);
+ }
+ } else {
+ throw new Error("no level loader defined");
+ }
+
+ return true;
+ };
+ /**
+ * return the current level id
+ * @name getCurrentLevelId
+ * @memberOf me.levelDirector
+ * @public
+ * @function
+ * @return {String}
+ */
+
+
+ api.getCurrentLevelId = function () {
+ return levelIdx[currentLevelIdx];
+ };
+ /**
+ * return the current level definition.
+ * for a reference to the live instantiated level,
+ * rather use the container in which it was loaded (e.g. me.game.world)
+ * @name getCurrentLevel
+ * @memberOf me.levelDirector
+ * @public
+ * @function
+ * @return {me.TMXTileMap}
+ */
+
+
+ api.getCurrentLevel = function () {
+ return levels[api.getCurrentLevelId()];
+ };
+ /**
+ * reload the current level
+ * @name reloadLevel
+ * @memberOf me.levelDirector
+ * @public
+ * @function
+ * @param {Object} [options] additional optional parameters
+ * @param {me.Container} [options.container=me.game.world] container in which to load the specified level
+ * @param {function} [options.onLoaded=me.game.onLevelLoaded] callback for when the level is fully loaded
+ * @param {boolean} [options.flatten=me.game.mergeGroup] if true, flatten all objects into the given container
+ */
+
+
+ api.reloadLevel = function (options) {
+ // reset the level to initial state
+ //levels[currentLevel].reset();
+ return api.loadLevel(api.getCurrentLevelId(), options);
+ };
+ /**
+ * load the next level
+ * @name nextLevel
+ * @memberOf me.levelDirector
+ * @public
+ * @function
+ * @param {Object} [options] additional optional parameters
+ * @param {me.Container} [options.container=me.game.world] container in which to load the specified level
+ * @param {function} [options.onLoaded=me.game.onLevelLoaded] callback for when the level is fully loaded
+ * @param {boolean} [options.flatten=me.game.mergeGroup] if true, flatten all objects into the given container
+ */
+
+
+ api.nextLevel = function (options) {
+ //go to the next level
+ if (currentLevelIdx + 1 < levelIdx.length) {
+ return api.loadLevel(levelIdx[currentLevelIdx + 1], options);
+ } else {
+ return false;
+ }
+ };
+ /**
+ * load the previous level
+ * @name previousLevel
+ * @memberOf me.levelDirector
+ * @public
+ * @function
+ * @param {Object} [options] additional optional parameters
+ * @param {me.Container} [options.container=me.game.world] container in which to load the specified level
+ * @param {function} [options.onLoaded=me.game.onLevelLoaded] callback for when the level is fully loaded
+ * @param {boolean} [options.flatten=me.game.mergeGroup] if true, flatten all objects into the given container
+ */
+
+
+ api.previousLevel = function (options) {
+ // go to previous level
+ if (currentLevelIdx - 1 >= 0) {
+ return api.loadLevel(levelIdx[currentLevelIdx - 1], options);
+ } else {
+ return false;
+ }
+ };
+ /**
+ * return the amount of level preloaded
+ * @name levelCount
+ * @memberOf me.levelDirector
+ * @public
+ * @function
+ */
+
+
+ api.levelCount = function () {
+ return levelIdx.length;
+ }; // return our object
+
+
+ return api;
+ }();
+ })();
+
+ /**
+ * Tween.js - Licensed under the MIT license
+ * https://github.com/tweenjs/tween.js
+ */
+
+ /* eslint-disable quotes, keyword-spacing, comma-spacing, no-return-assign */
+ (function () {
+ /**
+ * Javascript Tweening Engine
+ * author mr.doob / http://mrdoob.com
+ * author Robert Eisele / http://www.xarg.org
+ * author Philippe / http://philippe.elsass.me
+ * author Robert Penner / http://www.robertpenner.com/easing_terms_of_use.html
+ * author Paul Lewis / http://www.aerotwist.com/
+ * author lechecacharro
+ * author Josh Faul / http://jocafa.com/
+ * @class
+ * @memberOf me
+ * @constructor
+ * @param {Object} object object on which to apply the tween
+ * @example
+ * // add a tween to change the object pos.y variable to 200 in 3 seconds
+ * tween = new me.Tween(myObject.pos).to({y: 200}, 3000).onComplete(myFunc);
+ * tween.easing(me.Tween.Easing.Bounce.Out);
+ * tween.start();
+ */
+ me.Tween = function (object) {
+ var _object = null;
+ var _valuesStart = null;
+ var _valuesEnd = null;
+ var _valuesStartRepeat = null;
+ var _duration = null;
+ var _repeat = null;
+ var _yoyo = null;
+ var _delayTime = null;
+ var _startTime = null;
+ var _easingFunction = null;
+ var _interpolationFunction = null;
+ var _chainedTweens = null;
+ var _onStartCallback = null;
+ var _onStartCallbackFired = null;
+ var _onUpdateCallback = null;
+ var _onCompleteCallback = null;
+ var _tweenTimeTracker = null; // comply with the container contract
+
+ this.isRenderable = false;
+ /**
+ * @ignore
+ */
+
+ this._resumeCallback = function (elapsed) {
+ if (_startTime) {
+ _startTime += elapsed;
+ }
+ };
+ /**
+ * @ignore
+ */
+
+
+ this.setProperties = function (object) {
+ _object = object;
+ _valuesStart = {};
+ _valuesEnd = {};
+ _valuesStartRepeat = {};
+ _duration = 1000;
+ _repeat = 0;
+ _yoyo = false;
+ _delayTime = 0;
+ _startTime = null;
+ _easingFunction = me.Tween.Easing.Linear.None;
+ _interpolationFunction = me.Tween.Interpolation.Linear;
+ _chainedTweens = [];
+ _onStartCallback = null;
+ _onStartCallbackFired = false;
+ _onUpdateCallback = null;
+ _onCompleteCallback = null;
+ _tweenTimeTracker = me.timer.lastUpdate; // reset flags to default value
+
+ this.isPersistent = false; // this is not really supported
+
+ this.updateWhenPaused = false; // Set all starting values present on the target object
+
+ for (var field in object) {
+ if (_typeof(object) !== 'object') {
+ _valuesStart[field] = parseFloat(object[field]);
+ }
+ }
+ };
+
+ this.setProperties(object);
+ /**
+ * reset the tween object to default value
+ * @ignore
+ */
+
+ this.onResetEvent = function (object) {
+ this.setProperties(object);
+ };
+ /**
+ * subscribe to the resume event when added
+ * @ignore
+ */
+
+
+ this.onActivateEvent = function () {
+ me.event.subscribe(me.event.STATE_RESUME, this._resumeCallback);
+ };
+ /**
+ * Unsubscribe when tween is removed
+ * @ignore
+ */
+
+
+ this.onDeactivateEvent = function () {
+ me.event.unsubscribe(me.event.STATE_RESUME, this._resumeCallback);
+ };
+ /**
+ * object properties to be updated and duration
+ * @name me.Tween#to
+ * @public
+ * @function
+ * @param {Object} properties hash of properties
+ * @param {Number} [duration=1000] tween duration
+ */
+
+
+ this.to = function (properties, duration) {
+ if (duration !== undefined) {
+ _duration = duration;
+ }
+
+ _valuesEnd = properties;
+ return this;
+ };
+ /**
+ * start the tween
+ * @name me.Tween#start
+ * @public
+ * @function
+ */
+
+
+ this.start = function (_time) {
+ _onStartCallbackFired = false; // add the tween to the object pool on start
+
+ me.game.world.addChild(this);
+ _startTime = (typeof _time === 'undefined' ? me.timer.getTime() : _time) + _delayTime;
+
+ for (var property in _valuesEnd) {
+ // check if an Array was provided as property value
+ if (_valuesEnd[property] instanceof Array) {
+ if (_valuesEnd[property].length === 0) {
+ continue;
+ } // create a local copy of the Array with the start value at the front
+
+
+ _valuesEnd[property] = [_object[property]].concat(_valuesEnd[property]);
+ }
+
+ _valuesStart[property] = _object[property];
+
+ if (_valuesStart[property] instanceof Array === false) {
+ _valuesStart[property] *= 1.0; // Ensures we're using numbers, not strings
+ }
+
+ _valuesStartRepeat[property] = _valuesStart[property] || 0;
+ }
+
+ return this;
+ };
+ /**
+ * stop the tween
+ * @name me.Tween#stop
+ * @public
+ * @function
+ */
+
+
+ this.stop = function () {
+ // remove the tween from the world container
+ me.game.world.removeChildNow(this);
+ return this;
+ };
+ /**
+ * delay the tween
+ * @name me.Tween#delay
+ * @public
+ * @function
+ * @param {Number} amount delay amount expressed in milliseconds
+ */
+
+
+ this.delay = function (amount) {
+ _delayTime = amount;
+ return this;
+ };
+ /**
+ * Repeat the tween
+ * @name me.Tween#repeat
+ * @public
+ * @function
+ * @param {Number} times amount of times the tween should be repeated
+ */
+
+
+ this.repeat = function (times) {
+ _repeat = times;
+ return this;
+ };
+ /**
+ * Allows the tween to bounce back to their original value when finished.
+ * To be used together with repeat to create endless loops.
+ * @name me.Tween#yoyo
+ * @public
+ * @function
+ * @see me.Tween#repeat
+ * @param {Boolean} yoyo
+ */
+
+
+ this.yoyo = function (yoyo) {
+ _yoyo = yoyo;
+ return this;
+ };
+ /**
+ * set the easing function
+ * @name me.Tween#easing
+ * @public
+ * @function
+ * @param {me.Tween.Easing} fn easing function
+ */
+
+
+ this.easing = function (easing) {
+ if (typeof easing !== 'function') {
+ throw new Error("invalid easing function for me.Tween.easing()");
+ }
+
+ _easingFunction = easing;
+ return this;
+ };
+ /**
+ * set the interpolation function
+ * @name me.Tween#interpolation
+ * @public
+ * @function
+ * @param {me.Tween.Interpolation} fn interpolation function
+ */
+
+
+ this.interpolation = function (interpolation) {
+ _interpolationFunction = interpolation;
+ return this;
+ };
+ /**
+ * chain the tween
+ * @name me.Tween#chain
+ * @public
+ * @function
+ * @param {me.Tween} chainedTween Tween to be chained
+ */
+
+
+ this.chain = function () {
+ _chainedTweens = arguments;
+ return this;
+ };
+ /**
+ * onStart callback
+ * @name me.Tween#onStart
+ * @public
+ * @function
+ * @param {Function} onStartCallback callback
+ */
+
+
+ this.onStart = function (callback) {
+ _onStartCallback = callback;
+ return this;
+ };
+ /**
+ * onUpdate callback
+ * @name me.Tween#onUpdate
+ * @public
+ * @function
+ * @param {Function} onUpdateCallback callback
+ */
+
+
+ this.onUpdate = function (callback) {
+ _onUpdateCallback = callback;
+ return this;
+ };
+ /**
+ * onComplete callback
+ * @name me.Tween#onComplete
+ * @public
+ * @function
+ * @param {Function} onCompleteCallback callback
+ */
+
+
+ this.onComplete = function (callback) {
+ _onCompleteCallback = callback;
+ return this;
+ };
+ /** @ignore */
+
+
+ this.update = function (dt) {
+ // the original Tween implementation expect
+ // a timestamp and not a time delta
+ _tweenTimeTracker = me.timer.lastUpdate > _tweenTimeTracker ? me.timer.lastUpdate : _tweenTimeTracker + dt;
+ var time = _tweenTimeTracker;
+ var property;
+
+ if (time < _startTime) {
+ return true;
+ }
+
+ if (_onStartCallbackFired === false) {
+ if (_onStartCallback !== null) {
+ _onStartCallback.call(_object);
+ }
+
+ _onStartCallbackFired = true;
+ }
+
+ var elapsed = (time - _startTime) / _duration;
+ elapsed = elapsed > 1 ? 1 : elapsed;
+
+ var value = _easingFunction(elapsed);
+
+ for (property in _valuesEnd) {
+ var start = _valuesStart[property] || 0;
+ var end = _valuesEnd[property];
+
+ if (end instanceof Array) {
+ _object[property] = _interpolationFunction(end, value);
+ } else {
+ // Parses relative end values with start as base (e.g.: +10, -3)
+ if (typeof end === "string") {
+ end = start + parseFloat(end);
+ } // protect against non numeric properties.
+
+
+ if (typeof end === "number") {
+ _object[property] = start + (end - start) * value;
+ }
+ }
+ }
+
+ if (_onUpdateCallback !== null) {
+ _onUpdateCallback.call(_object, value);
+ }
+
+ if (elapsed === 1) {
+ if (_repeat > 0) {
+ if (isFinite(_repeat)) {
+ _repeat--;
+ } // reassign starting values, restart by making startTime = now
+
+
+ for (property in _valuesStartRepeat) {
+ if (typeof _valuesEnd[property] === "string") {
+ _valuesStartRepeat[property] = _valuesStartRepeat[property] + parseFloat(_valuesEnd[property]);
+ }
+
+ if (_yoyo) {
+ var tmp = _valuesStartRepeat[property];
+ _valuesStartRepeat[property] = _valuesEnd[property];
+ _valuesEnd[property] = tmp;
+ }
+
+ _valuesStart[property] = _valuesStartRepeat[property];
+ }
+
+ _startTime = time + _delayTime;
+ return true;
+ } else {
+ // remove the tween from the world container
+ me.game.world.removeChildNow(this);
+
+ if (_onCompleteCallback !== null) {
+ _onCompleteCallback.call(_object);
+ }
+
+ for (var i = 0, numChainedTweens = _chainedTweens.length; i < numChainedTweens; i++) {
+ _chainedTweens[i].start(time);
+ }
+
+ return false;
+ }
+ }
+
+ return true;
+ };
+ };
+ /**
+ * Easing Function :
+ *
+ * me.Tween.Easing.Quadratic.In
+ * me.Tween.Easing.Quadratic.Out
+ * me.Tween.Easing.Quadratic.InOut
+ * me.Tween.Easing.Cubic.In
+ * me.Tween.Easing.Cubic.Out
+ * me.Tween.Easing.Cubic.InOut
+ * me.Tween.Easing.Quartic.In
+ * me.Tween.Easing.Quartic.Out
+ * me.Tween.Easing.Quartic.InOut
+ * me.Tween.Easing.Quintic.In
+ * me.Tween.Easing.Quintic.Out
+ * me.Tween.Easing.Quintic.InOut
+ * me.Tween.Easing.Sinusoidal.In
+ * me.Tween.Easing.Sinusoidal.Out
+ * me.Tween.Easing.Sinusoidal.InOut
+ * me.Tween.Easing.Exponential.In
+ * me.Tween.Easing.Exponential.Out
+ * me.Tween.Easing.Exponential.InOut
+ * me.Tween.Easing.Circular.In
+ * me.Tween.Easing.Circular.Out
+ * me.Tween.Easing.Circular.InOut
+ * me.Tween.Easing.Elastic.In
+ * me.Tween.Easing.Elastic.Out
+ * me.Tween.Easing.Elastic.InOut
+ * me.Tween.Easing.Back.In
+ * me.Tween.Easing.Back.Out
+ * me.Tween.Easing.Back.InOut
+ * me.Tween.Easing.Bounce.In
+ * me.Tween.Easing.Bounce.Out
+ * me.Tween.Easing.Bounce.InOut
+ *
+ *
+ * me.Tween.Interpolation.Bezier
+ * me.Tween.Interpolation.CatmullRom
+ *
+ * This namespace is a container for all registered plugins.
+ * @see me.plugin.register
+ * @namespace me.plugins
+ * @memberOf me
+ */
+ me.plugins = {};
+ /**
+ * There is no constructor function for me.plugin
+ * @namespace me.plugin
+ * @memberOf me
+ */
+
+ me.plugin = function () {
+ // hold public stuff inside the singleton
+ var singleton = {};
+ /*--------------
+ PUBLIC
+ --------------*/
+
+ /**
+ * a base Object for plugin
+ * plugin must be installed using the register function
+ * @see me.plugin
+ * @class
+ * @extends me.Object
+ * @name plugin.Base
+ * @memberOf me
+ * @constructor
+ */
+
+ singleton.Base = me.Object.extend({
+ /** @ignore */
+ init: function init() {
+ /**
+ * define the minimum required version of melonJS
+ * this can be overridden by the plugin
+ * @public
+ * @type String
+ * @default "7.0.0"
+ * @name me.plugin.Base#version
+ */
+ this.version = "7.0.0";
+ }
+ });
+ /**
+ * patch a melonJS function
+ * @name patch
+ * @memberOf me.plugin
+ * @public
+ * @function
+ * @param {Object} proto target object
+ * @param {String} name target function
+ * @param {Function} fn replacement function
+ * @example
+ * // redefine the me.game.update function with a new one
+ * me.plugin.patch(me.game, "update", function () {
+ * // display something in the console
+ * console.log("duh");
+ * // call the original me.game.update function
+ * this._patched();
+ * });
+ */
+
+ singleton.patch = function (proto, name, fn) {
+ // use the object prototype if possible
+ if (typeof proto.prototype !== "undefined") {
+ proto = proto.prototype;
+ } // reuse the logic behind me.Object.extend
+
+
+ if (typeof proto[name] === "function") {
+ // save the original function
+ var _parent = proto[name]; // override the function with the new one
+
+ Object.defineProperty(proto, name, {
+ "configurable": true,
+ "value": function (name, fn) {
+ return function () {
+ this._patched = _parent;
+ var ret = fn.apply(this, arguments);
+ this._patched = null;
+ return ret;
+ };
+ }(name, fn)
+ });
+ } else {
+ throw new Error(name + " is not an existing function");
+ }
+ };
+ /**
+ * Register a plugin.
+ * @name register
+ * @memberOf me.plugin
+ * @see me.plugin.Base
+ * @public
+ * @function
+ * @param {me.plugin.Base} plugin Plugin to instiantiate and register
+ * @param {String} name
+ * @param {} [arguments...] all extra parameters will be passed to the plugin constructor
+ * @example
+ * // register a new plugin
+ * me.plugin.register(TestPlugin, "testPlugin");
+ * // the plugin then also become available
+ * // under then me.plugins namespace
+ * me.plugins.testPlugin.myfunction ();
+ */
+
+
+ singleton.register = function (plugin, name) {
+ // ensure me.plugin[name] is not already "used"
+ if (me.plugin[name]) {
+ throw new Error("plugin " + name + " already registered");
+ } // get extra arguments
+
+
+ var _args = [];
+
+ if (arguments.length > 2) {
+ // store extra arguments if any
+ _args = Array.prototype.slice.call(arguments, 1);
+ } // try to instantiate the plugin
+
+
+ _args[0] = plugin;
+ var instance = new (plugin.bind.apply(plugin, _args))(); // inheritance check
+
+ if (!instance || !(instance instanceof me.plugin.Base)) {
+ throw new Error("Plugin should extend the me.plugin.Base Class !");
+ } // compatibility testing
+
+
+ if (me.sys.checkVersion(instance.version) > 0) {
+ throw new Error("Plugin version mismatch, expected: " + instance.version + ", got: " + me.version);
+ } // create a reference to the new plugin
+
+
+ me.plugins[name] = instance;
+ }; // return our singleton
+
+
+ return singleton;
+ }();
+ })();
+
+ /**
+ * Used to make a game entity draggable
+ * @class
+ * @extends me.Entity
+ * @memberOf me
+ * @constructor
+ * @param {Number} x the x coordinates of the entity object
+ * @param {Number} y the y coordinates of the entity object
+ * @param {Object} settings Entity properties (see {@link me.Entity})
+ */
+ me.DraggableEntity = function (Entity, Input, Event, Vector) {
+ return Entity.extend({
+ /**
+ * Constructor
+ * @name init
+ * @memberOf me.DraggableEntity
+ * @function
+ * @param {Number} x the x postion of the entity
+ * @param {Number} y the y postion of the entity
+ * @param {Object} settings the additional entity settings
+ */
+ init: function init(x, y, settings) {
+ this._super(Entity, "init", [x, y, settings]);
+
+ this.dragging = false;
+ this.dragId = null;
+ this.grabOffset = new Vector(0, 0);
+ this.onPointerEvent = Input.registerPointerEvent;
+ this.removePointerEvent = Input.releasePointerEvent;
+ this.initEvents();
+ },
+
+ /**
+ * Initializes the events the modules needs to listen to
+ * It translates the pointer events to me.events
+ * in order to make them pass through the system and to make
+ * this module testable. Then we subscribe this module to the
+ * transformed events.
+ * @name initEvents
+ * @memberOf me.DraggableEntity
+ * @function
+ */
+ initEvents: function initEvents() {
+ var self = this;
+ /**
+ * @ignore
+ */
+
+ this.mouseDown = function (e) {
+ this.translatePointerEvent(e, Event.DRAGSTART);
+ };
+ /**
+ * @ignore
+ */
+
+
+ this.mouseUp = function (e) {
+ this.translatePointerEvent(e, Event.DRAGEND);
+ };
+
+ this.onPointerEvent("pointerdown", this, this.mouseDown.bind(this));
+ this.onPointerEvent("pointerup", this, this.mouseUp.bind(this));
+ this.onPointerEvent("pointercancel", this, this.mouseUp.bind(this));
+ Event.subscribe(Event.POINTERMOVE, this.dragMove.bind(this));
+ Event.subscribe(Event.DRAGSTART, function (e, draggable) {
+ if (draggable === self) {
+ self.dragStart(e);
+ }
+ });
+ Event.subscribe(Event.DRAGEND, function (e, draggable) {
+ if (draggable === self) {
+ self.dragEnd(e);
+ }
+ });
+ },
+
+ /**
+ * Translates a pointer event to a me.event
+ * @name translatePointerEvent
+ * @memberOf me.DraggableEntity
+ * @function
+ * @param {Object} e the pointer event you want to translate
+ * @param {String} translation the me.event you want to translate
+ * the event to
+ */
+ translatePointerEvent: function translatePointerEvent(e, translation) {
+ Event.publish(translation, [e, this]);
+ },
+
+ /**
+ * Gets called when the user starts dragging the entity
+ * @name dragStart
+ * @memberOf me.DraggableEntity
+ * @function
+ * @param {Object} x the pointer event
+ */
+ dragStart: function dragStart(e) {
+ if (this.dragging === false) {
+ this.dragging = true;
+ this.grabOffset.set(e.gameX, e.gameY);
+ this.grabOffset.sub(this.pos);
+ return false;
+ }
+ },
+
+ /**
+ * Gets called when the user drags this entity around
+ * @name dragMove
+ * @memberOf me.DraggableEntity
+ * @function
+ * @param {Object} x the pointer event
+ */
+ dragMove: function dragMove(e) {
+ if (this.dragging === true) {
+ this.pos.set(e.gameX, e.gameY, this.pos.z); //TODO : z ?
+
+ this.pos.sub(this.grabOffset);
+ }
+ },
+
+ /**
+ * Gets called when the user stops dragging the entity
+ * @name dragEnd
+ * @memberOf me.DraggableEntity
+ * @function
+ * @param {Object} x the pointer event
+ */
+ dragEnd: function dragEnd() {
+ if (this.dragging === true) {
+ this.dragging = false;
+ return false;
+ }
+ },
+
+ /**
+ * Destructor
+ * @name destroy
+ * @memberOf me.DraggableEntity
+ * @function
+ */
+ destroy: function destroy() {
+ Event.unsubscribe(Event.POINTERMOVE, this.dragMove);
+ Event.unsubscribe(Event.DRAGSTART, this.dragStart);
+ Event.unsubscribe(Event.DRAGEND, this.dragEnd);
+ this.removePointerEvent("pointerdown", this);
+ this.removePointerEvent("pointerup", this);
+ }
+ });
+ }(me.Entity, me.input, me.event, me.Vector2d);
+
+ /**
+ * Used to make a game entity a droptarget
+ * @class
+ * @extends me.Entity
+ * @memberOf me
+ * @constructor
+ * @param {Number} x the x coordinates of the entity object
+ * @param {Number} y the y coordinates of the entity object
+ * @param {Object} settings Entity properties (see {@link me.Entity})
+ */
+ me.DroptargetEntity = function (Entity, Event) {
+ return Entity.extend({
+ /**
+ * Constructor
+ * @name init
+ * @memberOf me.DroptargetEntity
+ * @function
+ * @param {Number} x the x postion of the entity
+ * @param {Number} y the y postion of the entity
+ * @param {Object} settings the additional entity settings
+ */
+ init: function init(x, y, settings) {
+ /**
+ * constant for the overlaps method
+ * @public
+ * @constant
+ * @type String
+ * @name CHECKMETHOD_OVERLAP
+ * @memberOf me.DroptargetEntity
+ */
+ this.CHECKMETHOD_OVERLAP = "overlaps";
+ /**
+ * constant for the contains method
+ * @public
+ * @constant
+ * @type String
+ * @name CHECKMETHOD_CONTAINS
+ * @memberOf me.DroptargetEntity
+ */
+
+ this.CHECKMETHOD_CONTAINS = "contains";
+ /**
+ * the checkmethod we want to use
+ * @public
+ * @constant
+ * @type String
+ * @name checkMethod
+ * @memberOf me.DroptargetEntity
+ */
+
+ this.checkMethod = null;
+
+ this._super(Entity, "init", [x, y, settings]);
+
+ Event.subscribe(Event.DRAGEND, this.checkOnMe.bind(this));
+ this.checkMethod = this[this.CHECKMETHOD_OVERLAP];
+ },
+
+ /**
+ * Sets the collision method which is going to be used to check a valid drop
+ * @name setCheckMethod
+ * @memberOf me.DroptargetEntity
+ * @function
+ * @param {Constant} checkMethod the checkmethod (defaults to CHECKMETHOD_OVERLAP)
+ */
+ setCheckMethod: function setCheckMethod(checkMethod) {
+ // We can improve this check,
+ // because now you can use every method in theory
+ if (typeof this[checkMethod] !== "undefined") {
+ this.checkMethod = this[checkMethod];
+ }
+ },
+
+ /**
+ * Checks if a dropped entity is dropped on the current entity
+ * @name checkOnMe
+ * @memberOf me.DroptargetEntity
+ * @function
+ * @param {Object} draggableEntity the draggable entity that is dropped
+ */
+ checkOnMe: function checkOnMe(e, draggableEntity) {
+ if (draggableEntity && this.checkMethod(draggableEntity.getBounds())) {
+ // call the drop method on the current entity
+ this.drop(draggableEntity);
+ }
+ },
+
+ /**
+ * Gets called when a draggable entity is dropped on the current entity
+ * @name drop
+ * @memberOf me.DroptargetEntity
+ * @function
+ * @param {Object} draggableEntity the draggable entity that is dropped
+ */
+ drop: function drop() {},
+
+ /**
+ * Destructor
+ * @name destroy
+ * @memberOf me.DroptargetEntity
+ * @function
+ */
+ destroy: function destroy() {
+ Event.unsubscribe(Event.DRAGEND, this.checkOnMe);
+ }
+ });
+ }(me.Entity, me.event);
+
+ (function () {
+ /**
+ * @class
+ * @extends me.Entity
+ * @memberOf me
+ * @constructor
+ * @param {Number} x the x coordinates of the entity object
+ * @param {Number} y the y coordinates of the entity object
+ * @param {Object} settings See {@link me.Entity}
+ */
+ me.CollectableEntity = me.Entity.extend({
+ /**
+ * @ignore
+ */
+ init: function init(x, y, settings) {
+ // call the super constructor
+ this._super(me.Entity, "init", [x, y, settings]);
+
+ this.body.collisionType = me.collision.types.COLLECTABLE_OBJECT;
+ }
+ });
+ })();
+
+ (function () {
+ /**
+ * @class
+ * @extends me.Entity
+ * @memberOf me
+ * @constructor
+ * @param {Number} x the x coordinates of the object
+ * @param {Number} y the y coordinates of the object
+ * @param {Object} settings See {@link me.Entity}
+ * @param {String} [settings.duration] Fade duration (in ms)
+ * @param {String|me.Color} [settings.color] Fade color
+ * @param {String} [settings.to] TMX level to load
+ * @param {String|me.Container} [settings.container] Target container. See {@link me.levelDirector.loadLevel}
+ * @param {Function} [settings.onLoaded] Level loaded callback. See {@link me.levelDirector.loadLevel}
+ * @param {Boolean} [settings.flatten] Flatten all objects into the target container. See {@link me.levelDirector.loadLevel}
+ * @param {Boolean} [settings.setViewportBounds] Resize the viewport to match the level. See {@link me.levelDirector.loadLevel}
+ * @example
+ * me.game.world.addChild(new me.LevelEntity(
+ * x, y, {
+ * "duration" : 250,
+ * "color" : "#000",
+ * "to" : "mymap2"
+ * }
+ * ));
+ */
+ me.LevelEntity = me.Entity.extend({
+ /**
+ * @ignore
+ */
+ init: function init(x, y, settings) {
+ this._super(me.Entity, "init", [x, y, settings]);
+
+ this.nextlevel = settings.to;
+ this.fade = settings.fade;
+ this.duration = settings.duration;
+ this.fading = false;
+ this.name = "levelEntity"; // a temp variable
+
+ this.gotolevel = settings.to; // Collect the defined level settings
+
+ this.loadLevelSettings = {};
+ ["container", "onLoaded", "flatten", "setViewportBounds"].forEach(function (v) {
+ if (typeof settings[v] !== "undefined") {
+ this.loadLevelSettings[v] = settings[v];
+ }
+ }.bind(this));
+ this.body.collisionType = me.collision.types.ACTION_OBJECT;
+ },
+
+ /**
+ * @ignore
+ */
+ getlevelSettings: function getlevelSettings() {
+ // Lookup for the container instance
+ if (typeof this.loadLevelSettings.container === "string") {
+ this.loadLevelSettings.container = me.game.world.getChildByName(this.loadLevelSettings.container)[0];
+ }
+
+ return this.loadLevelSettings;
+ },
+
+ /**
+ * @ignore
+ */
+ onFadeComplete: function onFadeComplete() {
+ me.levelDirector.loadLevel(this.gotolevel, this.getlevelSettings());
+ me.game.viewport.fadeOut(this.fade, this.duration);
+ },
+
+ /**
+ * go to the specified level
+ * @name goTo
+ * @memberOf me.LevelEntity
+ * @function
+ * @param {String} [level=this.nextlevel] name of the level to load
+ * @protected
+ */
+ goTo: function goTo(level) {
+ this.gotolevel = level || this.nextlevel; // load a level
+ //console.log("going to : ", to);
+
+ if (this.fade && this.duration) {
+ if (!this.fading) {
+ this.fading = true;
+ me.game.viewport.fadeIn(this.fade, this.duration, this.onFadeComplete.bind(this));
+ }
+ } else {
+ me.levelDirector.loadLevel(this.gotolevel, this.getlevelSettings());
+ }
+ },
+
+ /** @ignore */
+ onCollision: function onCollision() {
+ if (this.name === "levelEntity") {
+ this.goTo.apply(this);
+ }
+
+ return false;
+ }
+ });
+ })();
+
+ (function () {
+ // generate a default image for the particles
+ var pixel = function () {
+ var canvas = me.video.createCanvas(1, 1);
+ var context = canvas.getContext("2d");
+ context.fillStyle = "#fff";
+ context.fillRect(0, 0, 1, 1);
+ return canvas;
+ }();
+ /**
+ * me.ParticleEmitterSettings contains the default settings for me.ParticleEmitter.
+ *
+ * @protected
+ * @class
+ * @memberOf me
+ * @see me.ParticleEmitter
+ */
+
+
+ me.ParticleEmitterSettings = {
+ /**
+ * Width of the particle spawn area.
+ * @public
+ * @type Number
+ * @name width
+ * @memberOf me.ParticleEmitterSettings
+ * @default 0
+ */
+ width: 0,
+
+ /**
+ * Height of the particle spawn area.
+ * @public
+ * @type Number
+ * @name height
+ * @memberOf me.ParticleEmitterSettings
+ * @default 0
+ */
+ height: 0,
+
+ /**
+ * Image used for particles.
+ * @public
+ * @type CanvasImageSource
+ * @name image
+ * @memberOf me.ParticleEmitterSettings
+ * @default 1x1 white pixel
+ * @see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#canvasimagesource
+ */
+ image: pixel,
+
+ /**
+ * Total number of particles in the emitter.
+ * @public
+ * @type Number
+ * @name totalParticles
+ * @default 50
+ * @memberOf me.ParticleEmitterSettings
+ */
+ totalParticles: 50,
+
+ /**
+ * Start angle for particle launch in Radians.
+ * @public
+ * @type Number
+ * @name angle
+ * @default Math.PI / 2
+ * @memberOf me.ParticleEmitterSettings
+ */
+ angle: Math.PI / 2,
+
+ /**
+ * Variation in the start angle for particle launch in Radians.
+ * @public
+ * @type Number
+ * @name angleVariation
+ * @default 0
+ * @memberOf me.ParticleEmitterSettings
+ */
+ angleVariation: 0,
+
+ /**
+ * Minimum time each particle lives once it is emitted in ms.
+ * @public
+ * @type Number
+ * @name minLife
+ * @default 1000
+ * @memberOf me.ParticleEmitterSettings
+ */
+ minLife: 1000,
+
+ /**
+ * Maximum time each particle lives once it is emitted in ms.
+ * @public
+ * @type Number
+ * @name maxLife
+ * @default 3000
+ * @memberOf me.ParticleEmitterSettings
+ */
+ maxLife: 3000,
+
+ /**
+ * Start speed of particles.
+ * @public
+ * @type Number
+ * @name speed
+ * @default 2
+ * @memberOf me.ParticleEmitterSettings
+ */
+ speed: 2,
+
+ /**
+ * Variation in the start speed of particles.
+ * @public
+ * @type Number
+ * @name speedVariation
+ * @default 1
+ * @memberOf me.ParticleEmitterSettings
+ */
+ speedVariation: 1,
+
+ /**
+ * Minimum start rotation for particles sprites in Radians.
+ * @public
+ * @type Number
+ * @name minRotation
+ * @default 0
+ * @memberOf me.ParticleEmitterSettings
+ */
+ minRotation: 0,
+
+ /**
+ * Maximum start rotation for particles sprites in Radians.
+ * @public
+ * @type Number
+ * @name maxRotation
+ * @default 0
+ * @memberOf me.ParticleEmitterSettings
+ */
+ maxRotation: 0,
+
+ /**
+ * Minimum start scale ratio for particles (1 = no scaling).
+ * @public
+ * @type Number
+ * @name minStartScale
+ * @default 1
+ * @memberOf me.ParticleEmitterSettings
+ */
+ minStartScale: 1,
+
+ /**
+ * Maximum start scale ratio for particles (1 = no scaling).
+ * @public
+ * @type Number
+ * @name maxStartScale
+ * @default 1
+ * @memberOf me.ParticleEmitterSettings
+ */
+ maxStartScale: 1,
+
+ /**
+ * Minimum end scale ratio for particles.
+ * @public
+ * @type Number
+ * @name minEndScale
+ * @default 0
+ * @memberOf me.ParticleEmitterSettings
+ */
+ minEndScale: 0,
+
+ /**
+ * Maximum end scale ratio for particles.
+ * @public
+ * @type Number
+ * @name maxEndScale
+ * @default 0
+ * @memberOf me.ParticleEmitterSettings
+ */
+ maxEndScale: 0,
+
+ /**
+ * Vertical force (Gravity) for each particle.
+ * @public
+ * @type Number
+ * @name gravity
+ * @default 0
+ * @memberOf me.ParticleEmitterSettings
+ * @see me.sys.gravity
+ */
+ gravity: 0,
+
+ /**
+ * Horizontal force (like a Wind) for each particle.
+ * @public
+ * @type Number
+ * @name wind
+ * @default 0
+ * @memberOf me.ParticleEmitterSettings
+ */
+ wind: 0,
+
+ /**
+ * Update the rotation of particle in accordance the particle trajectory.
+ * The particle sprite should aim at zero angle (draw from left to right).
+ * Override the particle minRotation and maxRotation.
+ * @public
+ * @type Boolean
+ * @name followTrajectory
+ * @default false
+ * @memberOf me.ParticleEmitterSettings
+ */
+ followTrajectory: false,
+
+ /**
+ * Enable the Texture Additive by canvas composite operation (lighter).
+ * WARNING: Composite Operation may decreases performance!.
+ * @public
+ * @type Boolean
+ * @name textureAdditive
+ * @default false
+ * @memberOf me.ParticleEmitterSettings
+ */
+ textureAdditive: false,
+
+ /**
+ * Update particles only in the viewport, remove it when out of viewport.
+ * @public
+ * @type Boolean
+ * @name onlyInViewport
+ * @default true
+ * @memberOf me.ParticleEmitterSettings
+ */
+ onlyInViewport: true,
+
+ /**
+ * Render particles in screen space.
+ * @public
+ * @type Boolean
+ * @name floating
+ * @default false
+ * @memberOf me.ParticleEmitterSettings
+ */
+ floating: false,
+
+ /**
+ * Maximum number of particles launched each time in this emitter (used only if emitter is Stream).
+ * @public
+ * @type Number
+ * @name maxParticles
+ * @default 10
+ * @memberOf me.ParticleEmitterSettings
+ */
+ maxParticles: 10,
+
+ /**
+ * How often a particle is emitted in ms (used only if emitter is Stream).
+ * Necessary that value is greater than zero.
+ * @public
+ * @type Number
+ * @name frequency
+ * @default 100
+ * @memberOf me.ParticleEmitterSettings
+ */
+ frequency: 100,
+
+ /**
+ * Duration that the emitter releases particles in ms (used only if emitter is Stream).
+ * After this period, the emitter stop the launch of particles.
+ * @public
+ * @type Number
+ * @name duration
+ * @default Infinity
+ * @memberOf me.ParticleEmitterSettings
+ */
+ duration: Infinity,
+
+ /**
+ * Skip n frames after updating the particle system once.
+ * This can be used to reduce the performance impact of emitters with many particles.
+ * @public
+ * @type Number
+ * @name framesToSkip
+ * @default 0
+ * @memberOf me.ParticleEmitterSettings
+ */
+ framesToSkip: 0
+ };
+ /**
+ * Particle Emitter Object.
+ * @class
+ * @extends Rect
+ * @memberOf me
+ * @constructor
+ * @param {Number} x x-position of the particle emitter
+ * @param {Number} y y-position of the particle emitter
+ * @param {object} settings An object containing the settings for the particle emitter. See {@link me.ParticleEmitterSettings}
+ * @example
+ *
+ * // Create a basic emitter at position 100, 100
+ * var emitter = new me.ParticleEmitter(100, 100);
+ *
+ * // Adjust the emitter properties
+ * emitter.totalParticles = 200;
+ * emitter.minLife = 1000;
+ * emitter.maxLife = 3000;
+ * emitter.z = 10;
+ *
+ * // Add the emitter to the game world
+ * me.game.world.addChild(emitter);
+ *
+ * // Launch all particles one time and stop, like a explosion
+ * emitter.burstParticles();
+ *
+ * // Launch constantly the particles, like a fountain
+ * emitter.streamParticles();
+ *
+ * // At the end, remove emitter from the game world
+ * // call this in onDestroyEvent function
+ * me.game.world.removeChild(emitter);
+ *
+ */
+
+ me.ParticleEmitter = me.Rect.extend({
+ /**
+ * @ignore
+ */
+ init: function init(x, y, settings) {
+ // Emitter is Stream, launch particles constantly
+
+ /** @ignore */
+ this._stream = false; // Frequency timer (in ms) for emitter launch new particles
+ // used only in stream emitter
+
+ /** @ignore */
+
+ this._frequencyTimer = 0; // Time of live (in ms) for emitter launch new particles
+ // used only in stream emitter
+
+ /** @ignore */
+
+ this._durationTimer = 0; // Emitter is emitting particles
+
+ /** @ignore */
+
+ this._enabled = false; // Emitter will always update
+
+ this.isRenderable = false; // call the super constructor
+
+ this._super(me.Rect, "init", [x, y, Infinity, Infinity]); // don't sort the particles by z-index
+
+
+ this.autoSort = false;
+ this.container = new me.ParticleContainer(this);
+ /**
+ * @ignore
+ */
+
+ Object.defineProperty(this.pos, "z", {
+ /**
+ * @ignore
+ */
+ get: function () {
+ return this.container.pos.z;
+ }.bind(this),
+
+ /**
+ * @ignore
+ */
+ set: function (value) {
+ this.container.pos.z = value;
+ }.bind(this),
+ enumerable: true,
+ configurable: true
+ });
+ /**
+ * Floating property for particles, value is forwarded to the particle container
+ * @type Boolean
+ * @name floating
+ * @memberOf me.ParticleEmitter
+ */
+
+ Object.defineProperty(this, "floating", {
+ /**
+ * @ignore
+ */
+ get: function get() {
+ return this.container.floating;
+ },
+
+ /**
+ * @ignore
+ */
+ set: function set(value) {
+ this.container.floating = value;
+ },
+ enumerable: true,
+ configurable: true
+ }); // Reset the emitter to defaults
+
+ this.reset(settings);
+ },
+
+ /**
+ * @ignore
+ */
+ onActivateEvent: function onActivateEvent() {
+ this.ancestor.addChild(this.container);
+ this.container.pos.z = this.pos.z;
+
+ if (!this.ancestor.autoSort) {
+ this.ancestor.sort();
+ }
+ },
+
+ /**
+ * @ignore
+ */
+ onDeactivateEvent: function onDeactivateEvent() {
+ if (this.ancestor.hasChild(this.container)) {
+ this.ancestor.removeChildNow(this.container);
+ }
+ },
+
+ /**
+ * @ignore
+ */
+ destroy: function destroy() {
+ this.reset();
+ },
+
+ /**
+ * returns a random point inside the bounds x axis of this emitter
+ * @name getRandomPointX
+ * @memberOf me.ParticleEmitter
+ * @function
+ * @return {Number}
+ */
+ getRandomPointX: function getRandomPointX() {
+ return this.pos.x + me.Math.randomFloat(0, this.width);
+ },
+
+ /**
+ * returns a random point inside the bounds y axis of this emitter
+ * @name getRandomPointY
+ * @memberOf me.ParticleEmitter
+ * @function
+ * @return {Number}
+ */
+ getRandomPointY: function getRandomPointY() {
+ return this.pos.y + me.Math.randomFloat(0, this.height);
+ },
+
+ /**
+ * Reset the emitter with default values.
+ * @function
+ * @param {Object} settings [optional] object with emitter settings. See {@link me.ParticleEmitterSettings}
+ * @name reset
+ * @memberOf me.ParticleEmitter
+ */
+ reset: function reset(settings) {
+ // check if settings exists and create a dummy object if necessary
+ settings = settings || {};
+ var defaults = me.ParticleEmitterSettings;
+ var width = typeof settings.width === "number" ? settings.width : defaults.width;
+ var height = typeof settings.height === "number" ? settings.height : defaults.height;
+ this.resize(width, height);
+ Object.assign(this, defaults, settings); // reset particle container values
+
+ this.container.reset();
+ },
+ // Add count particles in the game world
+
+ /** @ignore */
+ addParticles: function addParticles(count) {
+ for (var i = 0; i < ~~count; i++) {
+ // Add particle to the container
+ var particle = me.pool.pull("me.Particle", this);
+ this.container.addChild(particle);
+ }
+ },
+
+ /**
+ * Emitter is of type stream and is launching particles
+ * @function
+ * @returns {Boolean} Emitter is Stream and is launching particles
+ * @name isRunning
+ * @memberOf me.ParticleEmitter
+ */
+ isRunning: function isRunning() {
+ return this._enabled && this._stream;
+ },
+
+ /**
+ * Launch particles from emitter constantly
+ * Particles example: Fountains
+ * @param {Number} duration [optional] time that the emitter releases particles in ms
+ * @function
+ * @name streamParticles
+ * @memberOf me.ParticleEmitter
+ */
+ streamParticles: function streamParticles(duration) {
+ this._enabled = true;
+ this._stream = true;
+ this.frequency = Math.max(this.frequency, 1);
+ this._durationTimer = typeof duration === "number" ? duration : this.duration;
+ },
+
+ /**
+ * Stop the emitter from generating new particles (used only if emitter is Stream)
+ * @function
+ * @name stopStream
+ * @memberOf me.ParticleEmitter
+ */
+ stopStream: function stopStream() {
+ this._enabled = false;
+ },
+
+ /**
+ * Launch all particles from emitter and stop
+ * Particles example: Explosions
+ * @param {Number} total [optional] number of particles to launch
+ * @function
+ * @name burstParticles
+ * @memberOf me.ParticleEmitter
+ */
+ burstParticles: function burstParticles(total) {
+ this._enabled = true;
+ this._stream = false;
+ this.addParticles(typeof total === "number" ? total : this.totalParticles);
+ this._enabled = false;
+ },
+
+ /**
+ * @ignore
+ */
+ update: function update(dt) {
+ // Launch new particles, if emitter is Stream
+ if (this._enabled && this._stream) {
+ // Check if the emitter has duration set
+ if (this._durationTimer !== Infinity) {
+ this._durationTimer -= dt;
+
+ if (this._durationTimer <= 0) {
+ this.stopStream();
+ return false;
+ }
+ } // Increase the emitter launcher timer
+
+
+ this._frequencyTimer += dt; // Check for new particles launch
+
+ var particlesCount = this.container.children.length;
+
+ if (particlesCount < this.totalParticles && this._frequencyTimer >= this.frequency) {
+ if (particlesCount + this.maxParticles <= this.totalParticles) {
+ this.addParticles(this.maxParticles);
+ } else {
+ this.addParticles(this.totalParticles - particlesCount);
+ }
+
+ this._frequencyTimer = 0;
+ }
+ }
+
+ return true;
+ }
+ });
+ })();
+
+ (function () {
+ /**
+ * Particle Container Object.
+ * @class
+ * @extends me.Container
+ * @memberOf me
+ * @constructor
+ * @param {me.ParticleEmitter} emitter the emitter which owns this container
+ */
+ me.ParticleContainer = me.Container.extend({
+ /**
+ * @ignore
+ */
+ init: function init(emitter) {
+ // call the super constructor
+ this._super(me.Container, "init", [me.game.viewport.pos.x, me.game.viewport.pos.y, me.game.viewport.width, me.game.viewport.height]); // don't sort the particles by z-index
+
+
+ this.autoSort = false; // count the updates
+
+ this._updateCount = 0; // internally store how much time was skipped when frames are skipped
+
+ this._dt = 0; // cache the emitter for later use
+
+ this._emitter = emitter;
+ this.autoTransform = false;
+ this.anchorPoint.set(0, 0);
+ this.isKinematic = true;
+ },
+
+ /**
+ * @ignore
+ */
+ update: function update(dt) {
+ // skip frames if necessary
+ if (++this._updateCount > this._emitter.framesToSkip) {
+ this._updateCount = 0;
+ }
+
+ if (this._updateCount > 0) {
+ this._dt += dt;
+ return false;
+ } // apply skipped delta time
+
+
+ dt += this._dt;
+ this._dt = 0; // Update particles and remove them if they are dead
+
+ var viewport = me.game.viewport;
+
+ for (var i = this.children.length - 1; i >= 0; --i) {
+ var particle = this.children[i];
+ particle.inViewport = viewport.isVisible(particle, this.floating);
+
+ if (!particle.update(dt)) {
+ this.removeChildNow(particle);
+ }
+ }
+
+ return true;
+ },
+
+ /**
+ * @ignore
+ */
+ draw: function draw(renderer, rect) {
+ if (this.children.length > 0) {
+ var context = renderer.getContext(),
+ gco; // Check for additive draw
+
+ if (this._emitter.textureAdditive) {
+ gco = context.globalCompositeOperation;
+ context.globalCompositeOperation = "lighter";
+ }
+
+ this._super(me.Container, "draw", [renderer, rect]); // Restore globalCompositeOperation
+
+
+ if (this._emitter.textureAdditive) {
+ context.globalCompositeOperation = gco;
+ }
+ }
+ }
+ });
+ })();
+
+ (function () {
+ /**
+ * Single Particle Object.
+ * @class
+ * @extends me.Renderable
+ * @memberOf me
+ * @constructor
+ * @param {me.ParticleEmitter} particle emitter
+ */
+ me.Particle = me.Renderable.extend({
+ /**
+ * @ignore
+ */
+ init: function init(emitter) {
+ // Call the super constructor
+ this._super(me.Renderable, "init", [emitter.getRandomPointX(), emitter.getRandomPointY(), emitter.image.width, emitter.image.height]); // Particle will always update
+
+
+ this.alwaysUpdate = true; // Cache the image reference
+
+ this.image = emitter.image; // Set the start particle Angle and Speed as defined in emitter
+
+ var angle = emitter.angle + (emitter.angleVariation > 0 ? (me.Math.randomFloat(0, 2) - 1) * emitter.angleVariation : 0);
+ var speed = emitter.speed + (emitter.speedVariation > 0 ? (me.Math.randomFloat(0, 2) - 1) * emitter.speedVariation : 0); // Set the start particle Velocity
+
+ this.vel = new me.Vector2d(speed * Math.cos(angle), -speed * Math.sin(angle)); // Set the start particle Time of Life as defined in emitter
+
+ this.life = me.Math.randomFloat(emitter.minLife, emitter.maxLife);
+ this.startLife = this.life; // Set the start and end particle Scale as defined in emitter
+ // clamp the values as minimum and maximum scales range
+
+ this.startScale = me.Math.clamp(me.Math.randomFloat(emitter.minStartScale, emitter.maxStartScale), emitter.minStartScale, emitter.maxStartScale);
+ this.endScale = me.Math.clamp(me.Math.randomFloat(emitter.minEndScale, emitter.maxEndScale), emitter.minEndScale, emitter.maxEndScale); // Set the particle Gravity and Wind (horizontal gravity) as defined in emitter
+
+ this.gravity = emitter.gravity;
+ this.wind = emitter.wind; // Set if the particle update the rotation in accordance the trajectory
+
+ this.followTrajectory = emitter.followTrajectory; // Set if the particle update only in Viewport
+
+ this.onlyInViewport = emitter.onlyInViewport; // Set the particle Z Order
+
+ this.pos.z = emitter.z; // cache inverse of the expected delta time
+
+ this._deltaInv = me.sys.fps / 1000; // Set the start particle rotation as defined in emitter
+ // if the particle not follow trajectory
+
+ if (!emitter.followTrajectory) {
+ this.angle = me.Math.randomFloat(emitter.minRotation, emitter.maxRotation);
+ }
+ },
+
+ /**
+ * Update the Particle
+ * This is automatically called by the game manager {@link me.game}
+ * @name update
+ * @memberOf me.Particle
+ * @function
+ * @ignore
+ * @param {Number} dt time since the last update in milliseconds
+ */
+ update: function update(dt) {
+ // move things forward independent of the current frame rate
+ var skew = dt * this._deltaInv; // Decrease particle life
+
+ this.life = this.life > dt ? this.life - dt : 0; // Calculate the particle Age Ratio
+
+ var ageRatio = this.life / this.startLife; // Resize the particle as particle Age Ratio
+
+ var scale = this.startScale;
+
+ if (this.startScale > this.endScale) {
+ scale *= ageRatio;
+ scale = scale < this.endScale ? this.endScale : scale;
+ } else if (this.startScale < this.endScale) {
+ scale /= ageRatio;
+ scale = scale > this.endScale ? this.endScale : scale;
+ } // Set the particle opacity as Age Ratio
+
+
+ this.alpha = ageRatio; // Adjust the particle velocity
+
+ this.vel.x += this.wind * skew;
+ this.vel.y += this.gravity * skew; // If necessary update the rotation of particle in accordance the particle trajectory
+
+ var angle = this.followTrajectory ? Math.atan2(this.vel.y, this.vel.x) : this.angle;
+ this.pos.x += this.vel.x * skew;
+ this.pos.y += this.vel.y * skew; // Update particle transform
+
+ this.currentTransform.setTransform(scale, 0, 0, 0, scale, 0, this.pos.x, this.pos.y, 1).rotate(angle); // Return true if the particle is not dead yet
+
+ return (this.inViewport || !this.onlyInViewport) && this.life > 0;
+ },
+
+ /**
+ * @ignore
+ */
+ preDraw: function preDraw(renderer) {
+ // restore is called in postDraw
+ renderer.save(); // particle alpha value
+
+ renderer.setGlobalAlpha(renderer.globalAlpha() * this.alpha); // translate to the defined anchor point and scale it
+
+ renderer.transform(this.currentTransform);
+ },
+
+ /**
+ * @ignore
+ */
+ draw: function draw(renderer) {
+ var w = this.width,
+ h = this.height;
+ renderer.drawImage(this.image, 0, 0, w, h, -w / 2, -h / 2, w, h);
+ }
+ });
+ })();
+
+ // placeholder for all deprecated classes,
+ // and corresponding alias for backward compatibility
+
+ /**
+ * @class me.ScreenObject
+ * @deprecated since 6.2.0
+ * @see me.Stage
+ */
+ me.ScreenObject = me.Stage.extend({
+ /** @ignore */
+ init: function init(settings) {
+ // super constructor
+ this._super(me.Stage, "init", settings); // deprecation warning
+
+
+ console.log("me.ScreenObject is deprecated, please use me.Stage");
+ }
+ });
+ /**
+ * @class me.Font
+ * @deprecated since 6.1.0
+ * @see me.Text
+ */
+
+ me.Font = me.Text.extend({
+ /** @ignore */
+ init: function init(font, size, fillStyle, textAlign) {
+ var settings = {
+ font: font,
+ size: size,
+ fillStyle: fillStyle,
+ textAlign: textAlign // super constructor
+
+ };
+
+ this._super(me.Text, "init", [0, 0, settings]); // deprecation warning
+
+
+ console.log("me.Font is deprecated, please use me.Text");
+ },
+
+ /** @ignore */
+ setFont: function setFont(font, size, fillStyle, textAlign) {
+ // apply fillstyle if defined
+ if (typeof fillStyle !== "undefined") {
+ this.fillStyle.copy(fillStyle);
+ } // h alignement if defined
+
+
+ if (typeof textAlign !== "undefined") {
+ this.textAlign = textAlign;
+ } // super constructor
+
+
+ return this._super(me.Text, "setFont", [font, size]);
+ }
+ });
+ /**
+ * @ignore
+ */
+
+ me.BitmapFontData = me.BitmapTextData;
+ /**
+ * @class me.BitmapFont
+ * @deprecated since 6.1.0
+ * @see me.BitmapText
+ */
+
+ me.BitmapFont = me.BitmapText.extend({
+ /** @ignore */
+ init: function init(data, fontImage, scale, textAlign, textBaseline) {
+ var settings = {
+ font: fontImage,
+ fontData: data,
+ size: scale,
+ textAlign: textAlign,
+ textBaseline: textBaseline // super constructor
+
+ };
+
+ this._super(me.BitmapText, "init", [0, 0, settings]); // deprecation warning
+
+
+ console.log("me.BitmapFont is deprecated, please use me.BitmapText");
+ }
+ });
+ /**
+ * @function me.Renderer.drawShape
+ * @deprecated since 6.3.0
+ * @see me.Renderer#stroke
+ */
+
+ me.Renderer.prototype.drawShape = function () {
+ console.log("drawShape() is deprecated, please use the stroke() or fill() function");
+ me.Renderer.prototype.stroke.apply(this, arguments);
+ };
+ /**
+ * @ignore
+ */
+
+
+ me.CanvasRenderer.prototype.Texture = me.Renderer.prototype.Texture;
+ /**
+ * @ignore
+ */
+
+ me.WebGLRenderer.prototype.Texture = me.Renderer.prototype.Texture;
+ /**
+ * @function me.video.getPos
+ * @deprecated since 7.0.0
+ * @see me.Renderer#getBounds
+ */
+
+ me.video.getPos = function () {
+ console.log("me.video.getPos() is deprecated, please use me.video.renderer.getBounds()");
+ return me.video.renderer.getBounds();
+ };
+ /**
+ * melonJS base class for exception handling.
+ * @class
+ * @extends me.Object
+ * @memberOf me
+ * @constructor
+ * @deprecated since 7.0.0
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
+ * @param {String} msg Error message.
+ */
+
+
+ me.Error = me.Object.extend.bind(Error)({
+ /**
+ * @ignore
+ */
+ init: function init(msg) {
+ this.name = "me.Error";
+ this.message = msg;
+ }
+ });
+
+}());
+//# sourceMappingURL=melonjs.js.map
diff --git a/dist/melonjs.min.js b/dist/melonjs.min.js
new file mode 100644
index 0000000000..53ea0ec8dc
--- /dev/null
+++ b/dist/melonjs.min.js
@@ -0,0 +1,36 @@
+/*!
+ * melonJS Game Engine - v7.0.0
+ * http://www.melonjs.org
+ * melonjs is licensed under the MIT License.
+ * http://www.opensource.org/licenses/mit-license
+ * @copyright (C) 2011 - 2019 Olivier Biot
+ */
+!function(){"use strict";!function(e){var t={};"function"==typeof define&&define.amd?define([],function(){return{me:t}}):"undefined"!=typeof exports&&("undefined"!=typeof module&&module.exports&&(exports=module.exports=t),exports.me=t),"undefined"!=typeof window?window.me=t:void 0!==e&&(e.me=t)}(window),"undefined"==typeof console&&(console={log:function(){},info:function(){},error:function(){alert(Array.prototype.slice.call(arguments).join(", "))}}),function(){var e,t=0,i=["ms","moz","webkit","o"],n=window.requestAnimationFrame,r=window.cancelAnimationFrame;for(e=0;e=n.next.y&&n.next.y!==n.y){var a=n.x+(s-n.y)*(n.next.x-n.x)/(n.next.y-n.y);if(a<=r&&a>o){if(o=a,a===r){if(s===n.y)return n;if(s===n.next.y)return n.next}i=n.x0;)s.callbacks[e].pop();0===s.callbacks[e].length&&delete s.callbacks[e]}0===Object.keys(s.callbacks).length&&i.delete(t)}}}(me.input),function(e){var t=.1;function i(e){return e}function n(t,i,n){return t=t>0?n===e.GAMEPAD.BUTTONS.L2?Math.max(0,t-2e4)/111070:(t-1)/131070:(65536+t)/131070+.5}var r=/^([0-9a-f]{1,4})-([0-9a-f]{1,4})-/i,s=/^0+/;function o(e,t){var n=e.replace(r,function(e,t,i){return"000".substr(t.length-1)+t+"-"+"000".substr(i.length-1)+i+"-"}),o=e.replace(r,function(e,t,i){return t.replace(s,"")+"-"+i.replace(s,"")+"-"});t.analog=t.analog||t.buttons.map(function(){return-1}),t.normalize_fn=t.normalize_fn||i,h.set(n,t),h.set(o,t)}var a={},h=new Map;[["45e-28e-Xbox 360 Wired Controller",{axes:[0,1,3,4],buttons:[11,12,13,14,8,9,-1,-1,5,4,6,7,0,1,2,3,10],analog:[-1,-1,-1,-1,-1,-1,2,5,-1,-1,-1,-1,-1,-1,-1,-1,-1],normalize_fn:function(t,i,n){return n===e.GAMEPAD.BUTTONS.L2||n===e.GAMEPAD.BUTTONS.R2?(t+1)/2:t}}],["54c-268-PLAYSTATION(R)3 Controller",{axes:[0,1,2,3],buttons:[14,13,15,12,10,11,8,9,0,3,1,2,4,6,7,5,16]}],["54c-5c4-Wireless Controller",{axes:[0,1,2,3],buttons:[1,0,2,3,4,5,6,7,8,9,10,11,14,15,16,17,12,13]}],["2836-1-OUYA Game Controller",{axes:[0,3,7,9],buttons:[3,6,4,5,7,8,15,16,-1,-1,9,10,11,12,13,14,-1],analog:[-1,-1,-1,-1,-1,-1,5,11,-1,-1,-1,-1,-1,-1,-1,-1,-1],normalize_fn:n}],["OUYA Game Controller (Vendor: 2836 Product: 0001)",{axes:[0,1,3,4],buttons:[0,3,1,2,4,5,12,13,-1,-1,6,7,8,9,10,11,-1],analog:[-1,-1,-1,-1,-1,-1,2,5,-1,-1,-1,-1,-1,-1,-1,-1,-1],normalize_fn:n}]].forEach(function(e){o(e[0],e[1])}),window.addEventListener("gamepadconnected",function(e){me.event.publish(me.event.GAMEPAD_CONNECTED,[e.gamepad])},!1),window.addEventListener("gamepaddisconnected",function(e){me.event.publish(me.event.GAMEPAD_DISCONNECTED,[e.gamepad])},!1),e._updateGamepads=navigator.getGamepads?function(){var i=navigator.getGamepads(),n={};Object.keys(a).forEach(function(r){var s=i[r];if(s){var o=null;"standard"!==s.mapping&&(o=h.get(s.id));var l=a[r];Object.keys(l.buttons).forEach(function(i){var a=l.buttons[i],h=i,u=-1;if(!(o&&(h=o.buttons[i],u=o.analog[i],h<0&&u<0))){var c=s.buttons[h]||{};if(o&&u>=0){var d=o.normalize_fn(s.axes[u],-1,+i);c={value:d,pressed:c.pressed||Math.abs(d)>=t}}me.event.publish(me.event.GAMEPAD_UPDATE,[r,"buttons",+i,c]),!a.pressed&&c.pressed?e._keydown(n,a.keyCode,h+256):a.pressed&&!c.pressed&&e._keyup(n,a.keyCode,h+256),a.value=c.value,a.pressed=c.pressed}}),Object.keys(l.axes).forEach(function(i){var a=l.axes[i],h=i;if(!(o&&(h=o.axes[i])<0)){var u=s.axes[h];if(void 0!==u){o&&(u=o.normalize_fn(u,+i,-1));var c=Math.sign(u)||1;if(0!==a[c].keyCode){var d=Math.abs(u)>=t+Math.abs(a[c].threshold);me.event.publish(me.event.GAMEPAD_UPDATE,[r,"axes",+i,u]),!a[c].pressed&&d?(a[-c].pressed&&(e._keyup(n,a[-c].keyCode,h+256),a[-c].value=0,a[-c].pressed=!1),e._keydown(n,a[c].keyCode,h+256)):!a[c].pressed&&!a[-c].pressed||d||(c=a[c].pressed?c:-c,e._keyup(n,a[c].keyCode,h+256)),a[c].value=u,a[c].pressed=d}}}})}})}:function(){},e.GAMEPAD={AXES:{LX:0,LY:1,RX:2,RY:3,EXTRA_1:4,EXTRA_2:5,EXTRA_3:6,EXTRA_4:7},BUTTONS:{FACE_1:0,FACE_2:1,FACE_3:2,FACE_4:3,L1:4,R1:5,L2:6,R2:7,SELECT:8,BACK:8,START:9,FORWARD:9,L3:10,R3:11,UP:12,DOWN:13,LEFT:14,RIGHT:15,HOME:16,EXTRA_1:17,EXTRA_2:18,EXTRA_3:19,EXTRA_4:20}},e.bindGamepad=function(t,i,n){if(!e._KeyBinding[n])throw new Error("no action defined for keycode "+n);a[t]||(a[t]={axes:{},buttons:{}});var r={keyCode:n,value:0,pressed:!1,threshold:i.threshold},s=a[t][i.type];if("buttons"===i.type)s[i.code]=r;else if("axes"===i.type){var o=Math.sign(i.threshold)||1;s[i.code]||(s[i.code]={});var h=s[i.code];h[o]=r,h[-o]||(h[-o]={keyCode:0,value:0,pressed:!1,threshold:-o})}},e.unbindGamepad=function(e,t){if(!a[e])throw new Error("no bindings for gamepad "+e);a[e].buttons[t]={}},e.setGamepadDeadzone=function(e){t=e},e.setGamepadMapping=o}(me.input),function(){var e,t,i;me.utils=(t="",i=0,(e={}).getPixels=function(e){if(e instanceof HTMLImageElement){var t=me.CanvasRenderer.getContext2d(me.video.createCanvas(e.width,e.height));return t.drawImage(e,0,0),t.getImageData(0,0,e.width,e.height)}return e.getContext("2d").getImageData(0,0,e.width,e.height)},e.resetGUID=function(e,n){t=me.utils.string.toHex(e.toString().toUpperCase()),i=n||0},e.createGUID=function(e){return i+=e||1,t+"-"+(e||i)},e)}(),function(e){var t=function(){var e={},t=/^.*(\\|\/|\:)/,i=/\.[^\.]*$/;return e.getBasename=function(e){return e.replace(t,"").replace(i,"")},e.getExtension=function(e){return e.substring(e.lastIndexOf(".")+1,e.length)},e}();e.file=t}(me.utils),function(e){var t=function(){var e={defer:function(e,t){var i=Array.prototype.slice.call(arguments,1);return setTimeout(e.bind.apply(e,i),.01)},throttle:function(e,t,i){var n,r=window.performance.now();return"boolean"!=typeof i&&(i=!1),function(){var s=window.performance.now(),o=s-r,a=arguments;if(!(o1?s(e[i],e[i-1],i-n):s(e[r],e[r+1>i?i:r+1],n-r)},Bezier:function(e,t){var i,n=0,r=e.length-1,s=Math.pow,o=me.Tween.Interpolation.Utils.Bernstein;for(i=0;i<=r;i++)n+=s(1-t,r-i)*s(t,i)*e[i]*o(r,i);return n},CatmullRom:function(e,t){var i=e.length-1,n=i*t,r=Math.floor(n),s=me.Tween.Interpolation.Utils.CatmullRom;return e[0]===e[i]?(t<0&&(r=Math.floor(n=i*(1+t))),s(e[(r-1+i)%i],e[r],e[(r+1)%i],e[(r+2)%i],n-r)):t<0?e[0]-(s(e[0],e[0],e[1],e[1],-n)-e[0]):t>1?e[i]-(s(e[i],e[i],e[i-1],e[i-1],n-i)-e[i]):s(e[r?r-1:0],e[r],e[i