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
+ * (this flag will be set to false if cookies are blocked) + * @type Boolean + * @readonly + * @name localStorage + * @memberOf me.device + */ + + api.localStorage = false; + /** + * Browser accelerometer capabilities + * @type Boolean + * @readonly + * @name hasAccelerometer + * @memberOf me.device + */ + + api.hasAccelerometer = false; + /** + * Browser device orientation + * @type Boolean + * @readonly + * @name hasDeviceOrientation + * @memberOf me.device + */ + + api.hasDeviceOrientation = false; + /** + * Browser full screen support + * @type Boolean + * @readonly + * @name hasFullscreenSupport + * @memberOf me.device + */ + + api.hasFullscreenSupport = false; + /** + * Browser pointerlock api support + * @type Boolean + * @readonly + * @name hasPointerLockSupport + * @memberOf me.device + */ + + api.hasPointerLockSupport = false; + /** + * Device WebAudio Support + * @type Boolean + * @readonly + * @name hasWebAudio + * @memberOf me.device + */ + + api.hasWebAudio = false; + /** + * Browser Base64 decoding capability + * @type Boolean + * @readonly + * @name nativeBase64 + * @memberOf me.device + */ + + api.nativeBase64 = typeof window.atob === "function"; + /** + * Return the maximum number of simultaneous touch contact points are supported by the current device. + * @type Number + * @readonly + * @name maxTouchPoints + * @memberOf me.device + * @example + * if (me.device.maxTouchPoints > 1) { + * // device supports multi-touch + * } + */ + + api.maxTouchPoints = 1; + /** + * Touch capabilities + * @type Boolean + * @readonly + * @name touch + * @memberOf me.device + */ + + api.touch = false; + /** + * W3C standard wheel events + * @type Boolean + * @readonly + * @name wheel + * @memberOf me.device + */ + + api.wheel = false; + /** + * equals to true if a mobile device
+ * (Android | iPhone | iPad | iPod | BlackBerry | Windows Phone | Kindle) + * @type Boolean + * @readonly + * @name isMobile + * @memberOf me.device + */ + + api.isMobile = false; + /** + * equals to true if the device is an iOS platform. + * @type Boolean + * @readonly + * @name iOS + * @memberOf me.device + */ + + api.iOS = false; + /** + * equals to true if the device is an Android platform. + * @type Boolean + * @readonly + * @name android + * @memberOf me.device + */ + + api.android = false; + /** + * equals to true if the device is an Android 2.x platform. + * @type Boolean + * @readonly + * @name android2 + * @memberOf me.device + */ + + api.android2 = false; + /** + * equals to true if the device is a Linux platform. + * @type Boolean + * @readonly + * @name linux + * @memberOf me.device + */ + + api.linux = false; + /** + * equals to true if the game is running under Ejecta. + * @type Boolean + * @readonly + * @see http://impactjs.com/ejecta + * @name ejecta + * @memberOf me.device + */ + + api.ejecta = false; + /** + * equals to true if the game is running under Wechat. + * @type Boolean + * @readonly + * @name isWeixin + * @memberOf me.device + */ + + api.isWeixin = false; + /** + * equals to true if the device is running on ChromeOS. + * @type Boolean + * @readonly + * @name chromeOS + * @memberOf me.device + */ + + api.chromeOS = false; + /** + * equals to true if the device is a Windows Phone platform. + * @type Boolean + * @readonly + * @name wp + * @memberOf me.device + */ + + api.wp = false; + /** + * equals to true if the device is a BlackBerry platform. + * @type Boolean + * @readonly + * @name BlackBerry + * @memberOf me.device + */ + + api.BlackBerry = false; + /** + * equals to true if the device is a Kindle platform. + * @type Boolean + * @readonly + * @name Kindle + * @memberOf me.device + */ + + api.Kindle = false; + /** + * contains the g-force acceleration along the x-axis. + * @public + * @type Number + * @readonly + * @name accelerationX + * @memberOf me.device + */ + + api.accelerationX = 0; + /** + * contains the g-force acceleration along the y-axis. + * @public + * @type Number + * @readonly + * @name accelerationY + * @memberOf me.device + */ + + api.accelerationY = 0; + /** + * contains the g-force acceleration along the z-axis. + * @public + * @type Number + * @readonly + * @name accelerationZ + * @memberOf me.device + */ + + api.accelerationZ = 0; + /** + * Device orientation Gamma property. Gives angle on tilting a portrait held phone left or right + * @public + * @type Number + * @readonly + * @name gamma + * @memberOf me.device + */ + + api.gamma = 0; + /** + * Device orientation Beta property. Gives angle on tilting a portrait held phone forward or backward + * @public + * @type Number + * @readonly + * @name beta + * @memberOf me.device + */ + + api.beta = 0; + /** + * Device orientation Alpha property. Gives angle based on the rotation of the phone around its z axis. + * The z-axis is perpendicular to the phone, facing out from the center of the screen. + * @public + * @type Number + * @readonly + * @name alpha + * @memberOf me.device + */ + + api.alpha = 0; + /** + * a string representing the preferred language of the user, usually the language of the browser UI. + * (will default to "en" if the information is not available) + * @public + * @type String + * @readonly + * @see http://www.w3schools.com/tags/ref_language_codes.asp + * @name language + * @memberOf me.device + */ + + api.language = navigator.language || navigator.browserLanguage || navigator.userLanguage || "en"; + /** + * specify a function to execute when the Device is fully loaded and ready + * @name onReady + * @memberOf me.device + * @function + * @param {Function} fn the function to be executed + * @example + * // small game skeleton + * var game = { + * // called by the me.device.onReady function + * onload : function () { + * // init video + * if (!me.video.init('screen', 640, 480, true)) { + * alert("Sorry but your browser does not support html 5 canvas."); + * return; + * } + * + * // initialize the "audio" + * me.audio.init("mp3,ogg"); + * + * // set callback for ressources loaded event + * me.loader.onload = this.loaded.bind(this); + * + * // set all ressources to be loaded + * me.loader.preload(game.assets); + * + * // load everything & display a loading screen + * me.state.change(me.state.LOADING); + * }, + * + * // callback when everything is loaded + * loaded : function () { + * // define stuff + * // .... + * + * // change to the menu screen + * me.state.change(me.state.PLAY); + * } + * }; // game + * + * // "bootstrap" + * me.device.onReady(function () { + * game.onload(); + * }); + */ + + api.onReady = function (fn) { + // If the DOM is already ready + if (isReady) { + // Execute the function immediately + fn.call(window, []); + } else { + // Add the function to the wait list + readyList.push(fn); // attach listeners if not yet done + + if (!readyBound) { + // directly call domReady if document is already "ready" + if (document.readyState === "complete") { + // defer the fn call to ensure our script is fully loaded + window.setTimeout(me.device._domReady, 0); + } else { + if (document.addEventListener) { + // Use the handy event callback + document.addEventListener("DOMContentLoaded", me.device._domReady, false); + } // A fallback to window.onload, that will always work + + + window.addEventListener("load", me.device._domReady, false); + } + + readyBound = true; + } + } + }; + /** + * enable/disable swipe on WebView. + * @name enableSwipe + * @memberOf me.device + * @function + * @param {boolean} [enable=true] enable or disable swipe. + */ + + + api.enableSwipe = function (enable) { + if (enable !== false) { + if (swipeEnabled === false) { + window.document.removeEventListener("touchmove", disableSwipeFn, false); + swipeEnabled = true; + } + } else if (swipeEnabled === true) { + window.document.addEventListener("touchmove", disableSwipeFn, false); + swipeEnabled = false; + } + }; + /** + * Triggers a fullscreen request. Requires fullscreen support from the browser/device. + * @name requestFullscreen + * @memberOf me.device + * @function + * @param {Object} [element=default canvas object] the element to be set in full-screen mode. + * @example + * // add a keyboard shortcut to toggle Fullscreen mode on/off + * me.input.bindKey(me.input.KEY.F, "toggleFullscreen"); + * me.event.subscribe(me.event.KEYDOWN, function (action, keyCode, edge) { + * // toggle fullscreen on/off + * if (action === "toggleFullscreen") { + * if (!me.device.isFullscreen) { + * me.device.requestFullscreen(); + * } else { + * me.device.exitFullscreen(); + * } + * } + * }); + */ + + + api.requestFullscreen = function (element) { + if (this.hasFullscreenSupport) { + element = element || me.video.getWrapper(); + element.requestFullscreen = me.agent.prefixed("requestFullscreen", element) || element.mozRequestFullScreen; + element.requestFullscreen(); + } + }; + /** + * Exit fullscreen mode. Requires fullscreen support from the browser/device. + * @name exitFullscreen + * @memberOf me.device + * @function + */ + + + api.exitFullscreen = function () { + if (this.hasFullscreenSupport) { + document.exitFullscreen(); + } + }; + /** + * Return a string representing the orientation of the device screen. + * It can be "any", "natural", "landscape", "portrait", "portrait-primary", "portrait-secondary", "landscape-primary", "landscape-secondary" + * @name getScreenOrientation + * @memberOf me.device + * @see https://developer.mozilla.org/en-US/docs/Web/API/Screen/orientation + * @function + * @return {String} the screen orientation + */ + + + api.getScreenOrientation = function () { + var PORTRAIT = "portrait"; + var LANDSCAPE = "landscape"; + var screen = window.screen; // first try using "standard" values + + if (typeof screen !== "undefined") { + var orientation = me.agent.prefixed("orientation", screen); + + if (typeof orientation !== "undefined" && _typeof(orientation.type === "string")) { + // Screen Orientation API specification + return orientation.type; + } else if (typeof orientation === "string") { + // moz/ms-orientation are strings + return orientation; + } + } // check using the deprecated API + + + if (typeof window.orientation === "number") { + return Math.abs(window.orientation) === 90 ? LANDSCAPE : PORTRAIT; + } // fallback to window size check + + + return window.outerWidth > window.outerHeight ? LANDSCAPE : PORTRAIT; + }; + /** + * locks the device screen into the specified orientation.
+ * This method only works for installed Web apps or for Web pages in full-screen mode. + * @name lockOrientation + * @memberOf me.device + * @see https://developer.mozilla.org/en-US/docs/Web/API/Screen/lockOrientation + * @function + * @return {Boolean} true if the orientation was unsuccessfully locked + */ + + + api.lockOrientation = function (orientation) { + var screen = window.screen; + + if (typeof screen !== "undefined") { + var lockOrientation = me.agent.prefixed("lockOrientation", screen); + + if (typeof lockOrientation !== "undefined") { + return lockOrientation(orientation); + } + } + + return false; + }; + /** + * unlocks the device screen into the specified orientation.
+ * This method only works for installed Web apps or for Web pages in full-screen mode. + * @name unlockOrientation + * @memberOf me.device + * @see https://developer.mozilla.org/en-US/docs/Web/API/Screen/lockOrientation + * @function + * @return {Boolean} true if the orientation was unsuccessfully unlocked + */ + + + api.unlockOrientation = function (orientation) { + var screen = window.screen; + + if (typeof screen !== "undefined") { + var unlockOrientation = me.agent.prefixed("unlockOrientation", screen); + + if (typeof unlockOrientation !== "undefined") { + return unlockOrientation(orientation); + } + } + + return false; + }; + /** + * return true if the device screen orientation is in Portrait mode + * @name isPortrait + * @memberOf me.device + * @function + * @return {Boolean} + */ + + + api.isPortrait = function () { + return me.device.getScreenOrientation().includes("portrait"); + }; + /** + * return true if the device screen orientation is in Portrait mode + * @name isLandscape + * @memberOf me.device + * @function + * @return {Boolean} + */ + + + api.isLandscape = function () { + return me.device.getScreenOrientation().includes("landscape"); + }; + /** + * return the device storage + * @name getStorage + * @memberOf me.device + * @function + * @param {String} [type="local"] + * @return me.save object + */ + + + api.getStorage = function (type) { + type = type || "local"; + + switch (type) { + case "local": + return me.save; + + default: + throw new Error("storage type " + type + " not supported"); + } + }; + /** + * return the highest precision format supported by this device for GL Shaders + * @name getMaxShaderPrecision + * @memberOf me.device + * @function + * @param {WebGLRenderingContext} gl + * @return {Boolean} "lowp", "mediump", or "highp" + */ + + + api.getMaxShaderPrecision = function (gl) { + if (gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT).precision > 0 && gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT).precision > 0) { + return "highp"; + } + + if (gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.MEDIUM_FLOAT).precision > 0 && gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT).precision > 0) { + return "mediump"; + } + + return "lowp"; + }; + /** + * Makes a request to bring this device window to the front. + * @name focus + * @memberOf me.device + * @function + * @example + * if (clicked) { + * me.device.focus(); + * } + */ + + + api.focus = function () { + if (typeof window.focus === "function") { + window.focus(); + } + }; + /** + * event management (Accelerometer) + * http://www.mobilexweb.com/samples/ball.html + * http://www.mobilexweb.com/blog/safari-ios-accelerometer-websockets-html5 + * @ignore + */ + + + function onDeviceMotion(e) { + // Accelerometer information + api.accelerationX = e.accelerationIncludingGravity.x; + api.accelerationY = e.accelerationIncludingGravity.y; + api.accelerationZ = e.accelerationIncludingGravity.z; + } + + function onDeviceRotate(e) { + api.gamma = e.gamma; + api.beta = e.beta; + api.alpha = e.alpha; + } + /** + * Enters pointer lock, requesting it from the user first. Works on supported devices & browsers + * Must be called in a click event or an event that requires user interaction. + * If you need to run handle events for errors or change of the pointer lock, see below. + * @name turnOnPointerLock + * @memberOf me.device + * @function + * @example + * document.addEventListener("pointerlockchange", pointerlockchange, false); + * document.addEventListener("mozpointerlockchange", pointerlockchange, false); + * document.addEventListener("webkitpointerlockchange", pointerlockchange, false); + * + * document.addEventListener("pointerlockerror", pointerlockerror, false); + * document.addEventListener("mozpointerlockerror", pointerlockerror, false); + * document.addEventListener("webkitpointerlockerror", pointerlockerror, false); + */ + + + api.turnOnPointerLock = function () { + if (this.hasPointerLockSupport) { + var element = me.video.getWrapper(); + + if (me.device.ua.match(/Firefox/i)) { + var fullscreenchange = function fullscreenchange() { + if ((me.agent.prefixed("fullscreenElement", document) || document.mozFullScreenElement) === element) { + document.removeEventListener("fullscreenchange", fullscreenchange); + document.removeEventListener("mozfullscreenchange", fullscreenchange); + element.requestPointerLock = me.agent.prefixed("requestPointerLock", element); + element.requestPointerLock(); + } + }; + + document.addEventListener("fullscreenchange", fullscreenchange, false); + document.addEventListener("mozfullscreenchange", fullscreenchange, false); + me.device.requestFullscreen(); + } else { + element.requestPointerLock(); + } + } + }; + /** + * Exits pointer lock. Works on supported devices & browsers + * @name turnOffPointerLock + * @memberOf me.device + * @function + */ + + + api.turnOffPointerLock = function () { + if (this.hasPointerLockSupport) { + document.exitPointerLock(); + } + }; + /** + * watch Accelerator event + * @name watchAccelerometer + * @memberOf me.device + * @public + * @function + * @return {Boolean} false if not supported by the device + */ + + + api.watchAccelerometer = function () { + if (me.device.hasAccelerometer) { + if (!accelInitialized) { + // add a listener for the devicemotion event + window.addEventListener("devicemotion", onDeviceMotion, false); + accelInitialized = true; + } + + return true; + } + + return false; + }; + /** + * unwatch Accelerometor event + * @name unwatchAccelerometer + * @memberOf me.device + * @public + * @function + */ + + + api.unwatchAccelerometer = function () { + if (accelInitialized) { + // remove the listener for the devicemotion event + window.removeEventListener("devicemotion", onDeviceMotion, false); + accelInitialized = false; + } + }; + /** + * watch the device orientation event + * @name watchDeviceOrientation + * @memberOf me.device + * @public + * @function + * @return {Boolean} false if not supported by the device + */ + + + api.watchDeviceOrientation = function () { + if (me.device.hasDeviceOrientation && !deviceOrientationInitialized) { + window.addEventListener("deviceorientation", onDeviceRotate, false); + deviceOrientationInitialized = true; + } + + return false; + }; + /** + * unwatch Device orientation event + * @name unwatchDeviceOrientation + * @memberOf me.device + * @public + * @function + */ + + + api.unwatchDeviceOrientation = function () { + if (deviceOrientationInitialized) { + window.removeEventListener("deviceorientation", onDeviceRotate, false); + deviceOrientationInitialized = false; + } + }; + /** + * the vibrate method pulses the vibration hardware on the device,
+ * If the device doesn't support vibration, this method has no effect.
+ * If a vibration pattern is already in progress when this method is called, + * the previous pattern is halted and the new one begins instead. + * @name vibrate + * @memberOf me.device + * @public + * @function + * @param {Number|Number[]} pattern pattern of vibration and pause intervals + * @example + * // vibrate for 1000 ms + * me.device.vibrate(1000); + * // or alternatively + * me.device.vibrate([1000]); + * // vibrate for 50 ms, be still for 100 ms, and then vibrate for 150 ms: + * me.device.vibrate([50, 100, 150]); + * // cancel any existing vibrations + * me.device.vibrate(0); + */ + + + api.vibrate = function (pattern) { + if (navigator.vibrate) { + navigator.vibrate(pattern); + } + }; + + return api; + }(); + /** + * Ratio of the resolution in physical pixels to the resolution in CSS pixels for the current display device. + * @name devicePixelRatio + * @memberOf me.device + * @public + * @type Number + * @readonly + * @return {Number} + */ + + + Object.defineProperty(me.device, "devicePixelRatio", { + /** + * @ignore + */ + get: function get() { + return window.devicePixelRatio || 1; + } + }); + /** + * Returns true if the browser/device is in full screen mode. + * @name isFullscreen + * @memberOf me.device + * @public + * @type Boolean + * @readonly + * @return {boolean} + */ + + Object.defineProperty(me.device, "isFullscreen", { + /** + * @ignore + */ + get: function get() { + if (me.device.hasFullscreenSupport) { + return !!(me.agent.prefixed("fullscreenElement", document) || document.mozFullScreenElement); + } else { + return false; + } + } + }); + /** + * Returns true if the browser/device has audio capabilities. + * @name sound + * @memberOf me.device + * @public + * @type Boolean + * @readonly + * @return {boolean} + */ + + Object.defineProperty(me.device, "sound", { + /** + * @ignore + */ + get: function get() { + return me.audio.hasAudio(); + } + }); + })(); + + var minpubsub_src = createCommonjsModule(function (module, exports) { + /*! + * MinPubSub + * Copyright(c) 2011 Daniel Lamb + * MIT Licensed + */ + (function (context) { + var MinPubSub = {}; + + // the topic/subscription hash + var cache = context.c_ || {}; //check for 'c_' cache for unit testing + + MinPubSub.publish = function ( /* String */ topic, /* Array? */ args) { + // summary: + // Publish some data on a named topic. + // topic: String + // The channel to publish on + // args: Array? + // The data to publish. Each array item is converted into an ordered + // arguments on the subscribed functions. + // + // example: + // Publish stuff on '/some/topic'. Anything subscribed will be called + // with a function signature like: function(a,b,c){ ... } + // + // publish('/some/topic', ['a','b','c']); + + var subs = cache[topic], + len = subs ? subs.length : 0; + + //can change loop or reverse array if the order matters + while (len--) { + subs[len].apply(context, args || []); + } + }; + + MinPubSub.subscribe = function ( /* String */ topic, /* Function */ callback) { + // summary: + // Register a callback on a named topic. + // topic: String + // The channel to subscribe to + // callback: Function + // The handler event. Anytime something is publish'ed on a + // subscribed channel, the callback will be called with the + // published array as ordered arguments. + // + // returns: Array + // A handle which can be used to unsubscribe this particular subscription. + // + // example: + // subscribe('/some/topic', function(a, b, c){ /* handle data */ }); + + if (!cache[topic]) { + cache[topic] = []; + } + cache[topic].push(callback); + return [topic, callback]; // Array + }; + + MinPubSub.unsubscribe = function ( /* Array */ handle, /* Function? */ callback) { + // summary: + // Disconnect a subscribed function for a topic. + // handle: Array + // The return value from a subscribe call. + // example: + // var handle = subscribe('/some/topic', function(){}); + // unsubscribe(handle); + + var subs = cache[callback ? handle : handle[0]], + callback = callback || handle[1], + len = subs ? subs.length : 0; + + while (len--) { + if (subs[len] === callback) { + subs.splice(len, 1); + } + } + }; + + // UMD definition to allow for CommonJS, AMD and legacy window + if (module.exports) { + // CommonJS, just export + module.exports = exports = MinPubSub; + } else if (typeof context === 'object') { + // If no AMD and we are in the browser, attach to window + context.publish = MinPubSub.publish; + context.subscribe = MinPubSub.subscribe; + context.unsubscribe = MinPubSub.unsubscribe; + } + + })(commonjsGlobal.window); + }); + + // external import + + (function () { + /** + * an event system based on a micro publish/subscribe messaging framework + * @namespace event + * @memberOf me + */ + me.event = function () { + // hold public stuff inside the singleton + var api = {}; + /* + * PUBLIC + */ + + /** + * Channel Constant when the game is paused
+ * Data passed : none
+ * @public + * @constant + * @type String + * @name STATE_PAUSE + * @memberOf me.event + * @see me.event.subscribe + */ + + api.STATE_PAUSE = "me.state.onPause"; + /** + * Channel Constant for when the game is resumed
+ * Data passed : {Number} time in ms the game was paused + * @public + * @constant + * @type String + * @name STATE_RESUME + * @memberOf me.event + * @see me.event.subscribe + */ + + api.STATE_RESUME = "me.state.onResume"; + /** + * Channel Constant when the game is stopped
+ * Data passed : none
+ * @public + * @constant + * @type String + * @name STATE_STOP + * @memberOf me.event + * @see me.event.subscribe + */ + + api.STATE_STOP = "me.state.onStop"; + /** + * Channel Constant for when the game is restarted
+ * Data passed : {Number} time in ms the game was stopped + * @public + * @constant + * @type String + * @name STATE_RESTART + * @memberOf me.event + * @see me.event.subscribe + */ + + api.STATE_RESTART = "me.state.onRestart"; + /** + * Channel Constant for when the game manager is initialized
+ * Data passed : none
+ * @public + * @constant + * @type String + * @name GAME_INIT + * @memberOf me.event + * @see me.event.subscribe + */ + + api.GAME_INIT = "me.game.onInit"; + /** + * Channel Constant for when the game manager is resetted
+ * Data passed : none
+ * @public + * @constant + * @type String + * @name GAME_RESET + * @memberOf me.event + * @see me.event.subscribe + */ + + api.GAME_RESET = "me.game.onReset"; + /** + * Channel Constant for when a level is loaded
+ * Data passed : {String} Level Name + * @public + * @constant + * @type String + * @name LEVEL_LOADED + * @memberOf me.event + * @see me.event.subscribe + */ + + api.LEVEL_LOADED = "me.game.onLevelLoaded"; + /** + * Channel Constant for when everything has loaded
+ * Data passed : none
+ * @public + * @constant + * @type String + * @name LOADER_COMPLETE + * @memberOf me.event + * @see me.event.subscribe + */ + + api.LOADER_COMPLETE = "me.loader.onload"; + /** + * Channel Constant for displaying a load progress indicator
+ * Data passed : {Number} [0 .. 1], {Resource} resource object
+ * @public + * @constant + * @type String + * @name LOADER_PROGRESS + * @memberOf me.event + * @see me.event.subscribe + */ + + api.LOADER_PROGRESS = "me.loader.onProgress"; + /** + * Channel Constant for pressing a binded key
+ * Data passed : {String} user-defined action, {Number} keyCode, + * {Boolean} edge state
+ * Edge-state is for detecting "locked" key bindings. When a locked key + * is pressed and held, the first event will have the third argument + * set true. Subsequent events will continue firing with the third + * argument set false. + * @public + * @constant + * @type String + * @name KEYDOWN + * @memberOf me.event + * @see me.event.subscribe + * @example + * me.input.bindKey(me.input.KEY.X, "jump", true); // Edge-triggered + * me.input.bindKey(me.input.KEY.Z, "shoot"); // Level-triggered + * me.event.subscribe(me.event.KEYDOWN, function (action, keyCode, edge) { + * // Checking bound keys + * if (action === "jump") { + * if (edge) { + * this.doJump(); + * } + * + * // Make character fall slower when holding the jump key + * this.vel.y = this.body.gravity; + * } + * }); + */ + + api.KEYDOWN = "me.input.keydown"; + /** + * Channel Constant for releasing a binded key
+ * Data passed : {String} user-defined action, {Number} keyCode + * @public + * @constant + * @type String + * @name KEYUP + * @memberOf me.event + * @see me.event.subscribe + * @example + * me.event.subscribe(me.event.KEYUP, function (action, keyCode) { + * // Checking unbound keys + * if (keyCode == me.input.KEY.ESC) { + * if (me.state.isPaused()) { + * me.state.resume(); + * } + * else { + * me.state.pause(); + * } + * } + * }); + */ + + api.KEYUP = "me.input.keyup"; + /** + * Channel Constant for when a gamepad is connected
+ * Data passed : {Object} gamepad object + * @public + * @constant + * @type String + * @name GAMEPAD_CONNECTED + * @memberOf me.event + * @see me.event.subscribe + */ + + api.GAMEPAD_CONNECTED = "gamepad.connected"; + /** + * Channel Constant for when a gamepad is disconnected
+ * Data passed : {Object} gamepad object + * @public + * @constant + * @type String + * @name GAMEPAD_DISCONNECTED + * @memberOf me.event + * @see me.event.subscribe + */ + + api.GAMEPAD_DISCONNECTED = "gamepad.disconnected"; + /** + * Channel Constant for when gamepad button/axis state is updated
+ * Data passed : {Number} index
+ * Data passed : {String} type : "axes" or "buttons"
+ * Data passed : {Number} button
+ * Data passed : {Number} current.value
+ * Data passed : {Boolean} current.pressed + * @public + * @constant + * @type String + * @name GAMEPAD_UPDATE + * @memberOf me.event + * @see me.event.subscribe + */ + + api.GAMEPAD_UPDATE = "gamepad.update"; + /** + * Channel Constant for pointermove events on the screen area
+ * Data passed : {me.Pointer} a Pointer object + * @public + * @constant + * @type String + * @name POINTERMOVE + * @memberOf me.event + * @see me.event.subscribe + */ + + api.POINTERMOVE = "me.event.pointermove"; + /** + * Channel Constant for dragstart events on a Draggable entity
+ * Data passed: + * {Object} the drag event
+ * {Object} the Draggable entity + * @public + * @constant + * @type String + * @name DRAGSTART + * @memberOf me.event + * @see me.event.subscribe + */ + + api.DRAGSTART = "me.game.dragstart"; + /** + * Channel Constant for dragend events on a Draggable entity
+ * Data passed: + * {Object} the drag event
+ * {Object} the Draggable entity + * @public + * @constant + * @type String + * @name DRAGEND + * @memberOf me.event + * @see me.event.subscribe + */ + + api.DRAGEND = "me.game.dragend"; + /** + * Channel Constant for when the (browser) window is resized
+ * Data passed : {Event} Event object + * @public + * @constant + * @type String + * @name WINDOW_ONRESIZE + * @memberOf me.event + * @see me.event.subscribe + */ + + api.WINDOW_ONRESIZE = "window.onresize"; + /** + * Channel Constant for when the canvas is resized
+ * (this usually follows a WINDOW_ONRESIZE event).
+ * Data passed : {Number} canvas width
+ * Data passed : {Number} canvas height + * @public + * @constant + * @type String + * @name CANVAS_ONRESIZE + * @memberOf me.event + * @see me.event.subscribe + */ + + api.CANVAS_ONRESIZE = "canvas.onresize"; + /** + * Channel Constant for when the viewport is resized
+ * (this usually follows a WINDOW_ONRESIZE event, when using the `flex` scaling mode is used and after the viewport was updated).
+ * Data passed : {Number} viewport width
+ * Data passed : {Number} viewport height + * @public + * @constant + * @type String + * @name VIEWPORT_ONRESIZE + * @memberOf me.event + * @see me.event.subscribe + */ + + api.VIEWPORT_ONRESIZE = "viewport.onresize"; + /** + * Channel Constant for when the device is rotated
+ * Data passed : {Event} Event object
+ * @public + * @constant + * @type String + * @name WINDOW_ONORIENTATION_CHANGE + * @memberOf me.event + * @see me.event.subscribe + */ + + api.WINDOW_ONORIENTATION_CHANGE = "window.orientationchange"; + /** + * Channel Constant for when the (browser) window is scrolled
+ * Data passed : {Event} Event object + * @public + * @constant + * @type String + * @name WINDOW_ONSCROLL + * @memberOf me.event + * @see me.event.subscribe + */ + + api.WINDOW_ONSCROLL = "window.onscroll"; + /** + * Channel Constant for when the viewport position is updated
+ * Data passed : {me.Vector2d} viewport position vector + * @public + * @constant + * @type String + * @name VIEWPORT_ONCHANGE + * @memberOf me.event + * @see me.event.subscribe + */ + + api.VIEWPORT_ONCHANGE = "viewport.onchange"; + /** + * Channel Constant for when WebGL context is lost
+ * Data passed : {me.WebGLRenderer} the current webgl renderer instance` + * @public + * @constant + * @type String + * @name WEBGL_ONCONTEXT_LOST + * @memberOf me.event + * @see me.event.subscribe + */ + + api.WEBGL_ONCONTEXT_LOST = "renderer.webglcontextlost"; + /** + * Channel Constant for when WebGL context is restored
+ * Data passed : {me.WebGLRenderer} the current webgl renderer instance` + * @public + * @constant + * @type String + * @name WEBGL_ONCONTEXT_RESTORED + * @memberOf me.event + * @see me.event.subscribe + */ + + api.WEBGL_ONCONTEXT_RESTORED = "renderer.webglcontextrestored"; + /** + * Publish some data on a channel + * @name publish + * @memberOf me.event + * @public + * @function + * @param {String} channel The channel to publish on + * @param {Array} arguments The data to publish + * + * @example Publish stuff on '/some/channel'. + * Anything subscribed will be called with a function + * signature like: function (a,b,c){ ... } + * + * me.event.publish("/some/channel", ["a","b","c"]); + * + */ + + api.publish = minpubsub_src.publish; + /** + * Register a callback on a named channel. + * @name subscribe + * @memberOf me.event + * @public + * @function + * @param {String} channel The channel to subscribe to + * @param {Function} callback The event handler, any time something is + * published on a subscribed channel, the callback will be called + * with the published array as ordered arguments + * @return {handle} A handle which can be used to unsubscribe this + * particular subscription + * @example + * me.event.subscribe("/some/channel", function (a, b, c){ doSomething(); }); + */ + + api.subscribe = minpubsub_src.subscribe; + /** + * Disconnect a subscribed function for a channel. + * @name unsubscribe + * @memberOf me.event + * @see me.event.subscribe + * @public + * @function + * @param {Array|String} handle The return value from a subscribe call or the + * name of a channel as a String + * @param {Function} [callback] The callback to be unsubscribed. + * @example + * var handle = me.event.subscribe("/some/channel", function (){}); + * me.event.unsubscribe(handle); + * + * // Or alternatively ... + * + * var callback = function (){}; + * me.event.subscribe("/some/channel", callback); + * me.event.unsubscribe("/some/channel", callback); + */ + + api.unsubscribe = minpubsub_src.unsubscribe; // return our object + + return api; + }(); + })(); + + (function () { + /** + * me.game represents your current game, it contains all the objects, + * tilemap layers, current viewport, collision map, etc...
+ * me.game is also responsible for updating (each frame) the object status + * and draw them
+ * @namespace me.game + * @memberOf me + */ + me.game = function () { + // hold public stuff in our singleton + var api = {}; + /* + * PRIVATE STUFF + */ + // flag to redraw the sprites + + var initialized = false; // to know when we have to refresh the display + + var isDirty = true; // always refresh the display when updatesPerSecond are lower than fps + + var isAlwaysDirty = false; // frame counter for frameSkipping + // reset the frame counter + + var frameCounter = 0; + var frameRate = 1; // time accumulation for multiple update calls + + var accumulator = 0.0; + var accumulatorMax = 0.0; + var accumulatorUpdateDelta = 0; // min update step size + + var stepSize = 1000 / 60; + var updateDelta = 0; + var lastUpdateStart = null; + var updateAverageDelta = 0; // reference to the renderer object + + var renderer = null; + /* + * PUBLIC STUFF + */ + + /** + * a reference to the current active stage "default" camera + * @public + * @type {me.Camera2d} + * @name viewport + * @memberOf me.game + */ + + api.viewport = undefined; + /** + * a reference to the game world,
+ * a world is a virtual environment containing all the game objects + * @public + * @type {me.Container} + * @name world + * @memberOf me.game + */ + + api.world = null; + /** + * when true, all objects will be added under the root world container.
+ * When false, a `me.Container` object will be created for each corresponding groups + * @public + * @type {boolean} + * @default true + * @name mergeGroup + * @memberOf me.game + */ + + api.mergeGroup = true; + /** + * Specify the property to be used when sorting entities. + * Accepted values : "x", "y", "z" + * @public + * @type {string} + * @default "z" + * @name sortOn + * @memberOf me.game + */ + + api.sortOn = "z"; + /** + * Fired when a level is fully loaded and
+ * and all entities instantiated.
+ * Additionnaly the level id will also be passed + * to the called function. + * @public + * @function + * @name onLevelLoaded + * @memberOf me.game + * @example + * // call myFunction () everytime a level is loaded + * me.game.onLevelLoaded = this.myFunction.bind(this); + */ + + api.onLevelLoaded = function () {}; + /** + * Provide an object hash with all tag parameters specified in the url. + * @property {Boolean} [hitbox=false] draw the hitbox in the debug panel (if enabled) + * @property {Boolean} [velocity=false] draw the entities velocity in the debug panel (if enabled) + * @property {Boolean} [quadtree=false] draw the quadtree in the debug panel (if enabled) + * @property {Boolean} [webgl=false] force the renderer to WebGL + * @property {Boolean} [debug=false] display the debug panel (if preloaded) + * @property {String} [debugToggleKey="s"] show/hide the debug panel (if preloaded) + * @public + * @type {Object} + * @name HASH + * @memberOf me.game + * @example + * // http://www.example.com/index.html#debug&hitbox=true&mytag=value + * console.log(me.game.HASH["mytag"]); //> "value" + */ + + + api.HASH = null; + /** + * Initialize the game manager + * @name init + * @memberOf me.game + * @private + * @ignore + * @function + * @param {Number} [width] width of the canvas + * @param {Number} [height] width of the canvas + * init function. + */ + + api.init = function (width, height) { + if (!initialized) { + // if no parameter specified use the system size + width = width || me.video.renderer.getWidth(); + height = height || me.video.renderer.getHeight(); // the root object of our world is an entity container + + api.world = new me.Container(0, 0, width, height, true); + api.world.name = "rootContainer"; // to mimic the previous behavior + + api.world.anchorPoint.set(0, 0); // initialize the collision system (the quadTree mostly) + + me.collision.init(); + renderer = me.video.renderer; // publish init notification + + me.event.publish(me.event.GAME_INIT); // make display dirty by default + + isDirty = true; // set as initialized + + initialized = true; + } + }; + /** + * reset the game Object manager
+ * destroy all current objects + * @name reset + * @memberOf me.game + * @public + * @function + */ + + + api.reset = function () { + // clear the quadtree + me.collision.quadTree.clear(); // remove all objects + + api.world.reset(); // reset the anchorPoint + + api.world.anchorPoint.set(0, 0); // point to the current active stage "default" camera + + api.viewport = me.state.current().cameras.get("default"); // publish reset notification + + me.event.publish(me.event.GAME_RESET); // Refresh internal variables for framerate limiting + + api.updateFrameRate(); + }; + /** + * Update the renderer framerate using the system config variables. + * @name updateFrameRate + * @memberOf me.game + * @public + * @function + * @see me.sys.fps + * @see me.sys.updatesPerSecond + */ + + + api.updateFrameRate = function () { + // reset the frame counter + frameCounter = 0; + frameRate = ~~(0.5 + 60 / me.sys.fps); // set step size based on the updatesPerSecond + + stepSize = 1000 / me.sys.updatesPerSecond; + accumulator = 0.0; + accumulatorMax = stepSize * 10; // display should always re-draw when update speed doesn't match fps + // this means the user intends to write position prediction drawing logic + + isAlwaysDirty = me.sys.fps > me.sys.updatesPerSecond; + }; + /** + * Returns the parent container of the specified Child in the game world + * @name getParentContainer + * @memberOf me.game + * @function + * @param {me.Renderable} child + * @return {me.Container} + */ + + + api.getParentContainer = function (child) { + return child.ancestor; + }; + /** + * force the redraw (not update) of all objects + * @name repaint + * @memberOf me.game + * @public + * @function + */ + + + api.repaint = function () { + isDirty = true; + }; + /** + * update all objects of the game manager + * @name update + * @memberOf me.game + * @private + * @ignore + * @function + * @param {Number} time current timestamp as provided by the RAF callback + * @param {me.Stage} stage the current stage + */ + + + api.update = function (time, stage) { + // handle frame skipping if required + if (++frameCounter % frameRate === 0) { + // reset the frame counter + frameCounter = 0; // update the timer + + me.timer.update(time); // update the gamepads + + me.input._updateGamepads(); + + accumulator += me.timer.getDelta(); + accumulator = Math.min(accumulator, accumulatorMax); + updateDelta = me.sys.interpolation ? me.timer.getDelta() : stepSize; + accumulatorUpdateDelta = me.sys.interpolation ? updateDelta : Math.max(updateDelta, updateAverageDelta); + + while (accumulator >= accumulatorUpdateDelta || me.sys.interpolation) { + lastUpdateStart = window.performance.now(); // clear the quadtree + + me.collision.quadTree.clear(); // insert the world container (children) into the quadtree + + me.collision.quadTree.insertContainer(api.world); // update all objects (and pass the elapsed time since last frame) + + isDirty = api.world.update(updateDelta) || isDirty; // update the camera/viewport + // iterate through all cameras + + stage.cameras.forEach(function (camera) { + if (camera.update(updateDelta)) { + isDirty = true; + } + }); + me.timer.lastUpdate = window.performance.now(); + updateAverageDelta = me.timer.lastUpdate - lastUpdateStart; + accumulator -= accumulatorUpdateDelta; + + if (me.sys.interpolation) { + accumulator = 0; + break; + } + } + } + }; + /** + * draw all existing objects + * @name draw + * @memberOf me.game + * @private + * @ignore + * @function + * @param {me.Stage} stage the current stage + */ + + + api.draw = function (stage) { + if (renderer.isContextValid === true && (isDirty || isAlwaysDirty)) { + // prepare renderer to draw a new frame + renderer.clear(); // iterate through all cameras + + stage.cameras.forEach(function (camera) { + // render the root container + camera.draw(renderer, me.game.world); + }); + isDirty = false; // flush/render our frame + + renderer.flush(); + } + }; // return our object + + + return api; + }(); + })(); + + (function () { + /** + * me global references + * @ignore + */ + me.mod = "melonJS"; + me.version = "7.0.0"; + /** + * global system settings and browser capabilities + * @namespace + */ + + me.sys = { + /* + * Global settings + */ + + /** + * Set game FPS limiting + * @see me.timer.tick + * @type {Number} + * @default 60 + * @memberOf me.sys + */ + fps: 60, + + /** + * Rate at which the game updates;
+ * may be greater than or lower than the fps + * @see me.timer.tick + * @type {Number} + * @default 60 + * @memberOf me.sys + */ + updatesPerSecond: 60, + + /** + * Enable/disable frame interpolation + * @see me.timer.tick + * @type {Boolean} + * @default false + * @memberOf me.sys + */ + interpolation: false, + + /** + * Global scaling factor + * @type {me.Vector2d} + * @default <0,0> + * @memberOf me.sys + */ + scale: null, + //initialized by me.video.init + + /** + * Global y axis gravity settings. + * (will override body gravity value if defined) + * @type {Number} + * @default undefined + * @memberOf me.sys + */ + gravity: undefined, + + /** + * Specify either to stop on audio loading error or not
+ * if true, melonJS will throw an exception and stop loading
+ * if false, melonJS will disable sounds and output a warning message + * in the console
+ * @type {Boolean} + * @default true + * @memberOf me.sys + */ + stopOnAudioError: true, + + /** + * Specify whether to pause the game when losing focus.
+ * @type {Boolean} + * @default true + * @memberOf me.sys + */ + pauseOnBlur: true, + + /** + * Specify whether to unpause the game when gaining focus.
+ * @type {Boolean} + * @default true + * @memberOf me.sys + */ + resumeOnFocus: true, + + /** + * Specify whether to automatically bring the window to the front.
+ * @type {Boolean} + * @default true + * @memberOf me.sys + */ + autoFocus: true, + + /** + * Specify whether to stop the game when losing focus or not
+ * The engine restarts on focus if this is enabled. + * @type {boolean} + * @default false + * @memberOf me.sys + */ + stopOnBlur: false, + + /** + * Specify the rendering method for layers
+ * if false, visible part of the layers are rendered dynamically
+ * if true, the entire layers are first rendered into an offscreen + * canvas
+ * the "best" rendering method depends of your game
+ * (amount of layer, layer size, amount of tiles per layer, etc.)
+ * note : rendering method is also configurable per layer by adding this + * property to your layer (in Tiled)
+ * @type {Boolean} + * @default false + * @memberOf me.sys + */ + preRender: false, + + /* + * System methods + */ + + /** + * Compare two version strings + * @public + * @function + * @param {String} first First version string to compare + * @param {String} [second="7.0.0"] Second version string to compare + * @return {Number} comparison result
< 0 : first < second
+ * 0 : first == second
+ * > 0 : first > second + * @example + * if (me.sys.checkVersion("7.0.0") > 0) { + * console.error( + * "melonJS is too old. Expected: 7.0.0, Got: " + me.version + * ); + * } + */ + checkVersion: function checkVersion(first, second) { + second = second || me.version; + var a = first.split("."); + var b = second.split("."); + var len = Math.min(a.length, b.length); + var result = 0; + + for (var i = 0; i < len; i++) { + if (result = +a[i] - +b[i]) { + break; + } + } + + return result ? result : a.length - b.length; + } + }; + + function parseHash() { + var hash = {}; // No "document.location" exist for Wechat mini game platform. + + if (document.location && document.location.hash) { + document.location.hash.substr(1).split("&").filter(function (value) { + return value !== ""; + }).forEach(function (value) { + var kv = value.split("="); + var k = kv.shift(); + var v = kv.join("="); + hash[k] = v || true; + }); + } + + return hash; + } // a flag to know if melonJS + // is initialized + + + var me_initialized = false; + Object.defineProperty(me, "initialized", { + /** + * @ignore + */ + get: function get() { + return me_initialized; + } + }); + /** + * Disable melonJS auto-initialization + * @type {Boolean} + * @default false + * @memberOf me + */ + + me.skipAutoInit = false; + /** + * initial boot function + * @ignore + */ + + me.boot = function () { + // don't do anything if already initialized (should not happen anyway) + if (me_initialized) { + return; + } // check the device capabilites + + + me.device._check(); // init the object Pool + + + me.pool.init(); // initialize me.save + + me.save._init(); // parse optional url parameters/tags + + + me.game.HASH = parseHash(); // enable/disable the cache + + me.loader.setNocache(me.game.HASH.nocache || false); // init the FPS counter if needed + + me.timer.init(); // init the App Manager + + me.state.init(); // automatically enable keyboard events if on desktop + + if (me.device.isMobile === false) { + me.input._enableKeyboardEvent(); + } // init the level Director + + + me.levelDirector.reset(); + me_initialized = true; + }; // call the library init function when ready + + + if (me.skipAutoInit === false) { + me.device.onReady(function () { + me.boot(); + }); + } else { + /** + * @ignore + */ + me.init = function () { + me.boot(); + + me.device._domReady(); + }; + } + })(); + + (function () { + /** + * a Timer object to manage time function (FPS, Game Tick, Time...)

+ * 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 :
+ *


+ * A polygon's `winding` is clockwise iff its vertices (points) are declared turning to the right. The image above shows COUNTERCLOCKWISE winding. + * @class + * @extends me.Object + * @memberOf me + * @constructor + * @param {Number} x origin point of the Polygon + * @param {Number} y origin point of the Polygon + * @param {me.Vector2d[]} points array of vector defining the Polygon + */ + me.Polygon = me.Object.extend({ + /** + * @ignore + */ + init: function init(x, y, points) { + /** + * origin point of the Polygon + * @public + * @type {me.Vector2d} + * @name pos + * @memberof me.Polygon# + */ + this.pos = new me.Vector2d(); + /** + * The bounding rectangle for this shape + * @ignore + * @type {me.Rect} + * @name _bounds + * @memberOf me.Polygon# + */ + + this._bounds = undefined; + /** + * Array of points defining the Polygon
+ * Note: If you manually change `points`, you **must** call `recalc`afterwards so that the changes get applied correctly. + * @public + * @type {me.Vector2d[]} + * @name points + * @memberOf me.Polygon# + */ + + this.points = null; + /** + * The edges here are the direction of the `n`th edge of the polygon, relative to + * the `n`th point. If you want to draw a given edge from the edge value, you must + * first translate to the position of the starting point. + * @ignore + */ + + this.edges = []; + /** + * a list of indices for all vertices composing this polygon (@see earcut) + * @ignore + */ + + this.indices = []; + /** + * The normals here are the direction of the normal for the `n`th edge of the polygon, relative + * to the position of the `n`th point. If you want to draw an edge normal, you must first + * translate to the position of the starting point. + * @ignore + */ + + this.normals = []; // the shape type + + this.shapeType = "Polygon"; + this.setShape(x, y, points); + }, + + /** @ignore */ + onResetEvent: function onResetEvent(x, y, points) { + this.setShape(x, y, points); + }, + + /** + * set new value to the Polygon + * @name setShape + * @memberOf me.Polygon.prototype + * @function + * @param {Number} x position of the Polygon + * @param {Number} y position of the Polygon + * @param {me.Vector2d[]} points array of vector defining the Polygon + */ + setShape: function setShape(x, y, points) { + this.pos.set(x, y); + + if (!Array.isArray(points)) { + return this; + } // convert given points to me.Vector2d if required + + + if (!(points[0] instanceof me.Vector2d)) { + var _points = this.points = []; + + points.forEach(function (point) { + _points.push(new me.Vector2d(point.x, point.y)); + }); + } else { + // array of me.Vector2d + this.points = points; + } + + this.recalc(); + this.updateBounds(); + return this; + }, + + /** + * apply the given transformation matrix to this Polygon + * @name transform + * @memberOf me.Polygon.prototype + * @function + * @param {me.Matrix2d} matrix the transformation matrix + * @return {me.Polygon} Reference to this object for method chaining + */ + transform: function transform(m) { + var points = this.points; + var len = points.length; + + for (var i = 0; i < len; i++) { + m.multiplyVector(points[i]); + } + + this.recalc(); + this.updateBounds(); + return this; + }, + + /** + * apply an isometric projection to this shape + * @name toIso + * @memberOf me.Polygon.prototype + * @function + * @return {me.Polygon} Reference to this object for method chaining + */ + toIso: function toIso() { + return this.rotate(Math.PI / 4).scale(Math.SQRT2, Math.SQRT1_2); + }, + + /** + * apply a 2d projection to this shape + * @name to2d + * @memberOf me.Polygon.prototype + * @function + * @return {me.Polygon} Reference to this object for method chaining + */ + to2d: function to2d() { + return this.scale(Math.SQRT1_2, Math.SQRT2).rotate(-Math.PI / 4); + }, + + /** + * Rotate this Polygon (counter-clockwise) by the specified angle (in radians). + * @name rotate + * @memberOf me.Polygon.prototype + * @function + * @param {Number} angle The angle to rotate (in radians) + * @return {me.Polygon} Reference to this object for method chaining + */ + rotate: function rotate(angle) { + if (angle !== 0) { + var points = this.points; + var len = points.length; + + for (var i = 0; i < len; i++) { + points[i].rotate(angle); + } + + this.recalc(); + this.updateBounds(); + } + + return this; + }, + + /** + * Scale this Polygon by the given scalar. + * @name scale + * @memberOf me.Polygon.prototype + * @function + * @param {Number} x + * @param {Number} [y=x] + * @return {me.Polygon} Reference to this object for method chaining + */ + scale: function scale(x, y) { + y = typeof y !== "undefined" ? y : x; + var points = this.points; + var len = points.length; + + for (var i = 0; i < len; i++) { + points[i].scale(x, y); + } + + this.recalc(); + this.updateBounds(); + return this; + }, + + /** + * Scale this Polygon by the given vector + * @name scaleV + * @memberOf me.Polygon.prototype + * @function + * @param {me.Vector2d} v + * @return {me.Polygon} Reference to this object for method chaining + */ + scaleV: function scaleV(v) { + return this.scale(v.x, v.y); + }, + + /** + * Computes the calculated collision polygon. + * This **must** be called if the `points` array, `angle`, or `offset` is modified manually. + * @name recalc + * @memberOf me.Polygon.prototype + * @function + * @return {me.Polygon} Reference to this object for method chaining + */ + recalc: function recalc() { + var i; + var edges = this.edges; + var normals = this.normals; + var indices = this.indices; // Copy the original points array and apply the offset/angle + + var points = this.points; + var len = points.length; + + if (len < 3) { + throw new Error("Requires at least 3 points"); + } // Calculate the edges/normals + + + for (i = 0; i < len; i++) { + if (edges[i] === undefined) { + edges[i] = new me.Vector2d(); + } + + edges[i].copy(points[(i + 1) % len]).sub(points[i]); + + if (normals[i] === undefined) { + normals[i] = new me.Vector2d(); + } + + normals[i].copy(edges[i]).perp().normalize(); + } // trunc array + + + edges.length = len; + normals.length = len; // do not do anything here, indices will be computed by + // toIndices if array is empty upon function call + + indices.length = 0; + return this; + }, + + /** + * returns a list of indices for all triangles defined in this polygon + * @name getIndices + * @memberOf me.Polygon.prototype + * @function + * @param {Vector2d[]} a list of vector + * @return {me.Polygon} this Polygon + */ + getIndices: function getIndices(x, y) { + if (this.indices.length === 0) { + var points = this.points; + var data = []; // flatten the points vector array + + for (var i = 0; i < points.length; i++) { + // XXX Optimize me + data.push(points[i].x); + data.push(points[i].y); + } + + this.indices = earcut_1(data); + } + + return this.indices; + }, + + /** + * translate the Polygon by the specified offset + * @name translate + * @memberOf me.Polygon.prototype + * @function + * @param {Number} x x offset + * @param {Number} y y offset + * @return {me.Polygon} this Polygon + */ + translate: function translate(x, y) { + this.pos.x += x; + this.pos.y += y; + + this._bounds.translate(x, y); + + return this; + }, + + /** + * translate the Polygon by the specified vector + * @name translateV + * @memberOf me.Polygon.prototype + * @function + * @param {me.Vector2d} v vector offset + * @return {me.Polygon} Reference to this object for method chaining + */ + translateV: function translateV(v) { + this.pos.add(v); + + this._bounds.translateV(v); + + return this; + }, + + /** + * check if this Polygon contains the specified point + * @name containsPointV + * @memberOf me.Polygon.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 Polygon contains the specified point
+ * (Note: it is highly recommended to first do a hit test on the corresponding
+ * bounding rect, as the function can be highly consuming with complex shapes) + * @name containsPoint + * @memberOf me.Polygon.prototype + * @function + * @param {Number} x x coordinate + * @param {Number} y y coordinate + * @return {boolean} true if contains + */ + containsPoint: function containsPoint(x, y) { + var intersects = false; + var posx = this.pos.x, + posy = this.pos.y; + var points = this.points; + var len = points.length; //http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html + + for (var i = 0, j = len - 1; i < len; j = i++) { + var iy = points[i].y + posy, + ix = points[i].x + posx, + jy = points[j].y + posy, + jx = points[j].x + posx; + + if (iy > y !== jy > y && x < (jx - ix) * (y - iy) / (jy - iy) + ix) { + intersects = !intersects; + } + } + + return intersects; + }, + + /** + * returns the bounding box for this shape, the smallest Rectangle object completely containing this shape. + * @name getBounds + * @memberOf me.Polygon.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.Polygon.prototype + * @function + * @return {me.Rect} this shape bounding box Rectangle object + */ + updateBounds: function updateBounds() { + if (!this._bounds) { + this._bounds = new me.Rect(0, 0, 0, 0); + } + + this._bounds.setPoints(this.points); + + this._bounds.translateV(this.pos); + + return this._bounds; + }, + + /** + * clone this Polygon + * @name clone + * @memberOf me.Polygon.prototype + * @function + * @return {me.Polygon} new Polygon + */ + clone: function clone() { + var copy = []; + this.points.forEach(function (point) { + copy.push(point.clone()); + }); + return new me.Polygon(this.pos.x, this.pos.y, copy); + } + }); + })(); + + (function () { + /** + * a rectangle Object + * @class + * @extends me.Polygon + * @memberOf me + * @constructor + * @param {Number} x position of the Rectangle + * @param {Number} y position of the Rectangle + * @param {Number} w width of the rectangle + * @param {Number} h height of the rectangle + */ + me.Rect = me.Polygon.extend({ + /** + * @ignore + */ + init: function init(x, y, w, h) { + this._super(me.Polygon, "init", [x, y, [new me.Vector2d(0, 0), // 0, 0 + new me.Vector2d(w, 0), // 1, 0 + new me.Vector2d(w, h), // 1, 1 + new me.Vector2d(0, h) // 0, 1 + ]]); + + this.shapeType = "Rectangle"; + }, + + /** @ignore */ + onResetEvent: function onResetEvent(x, y, w, h) { + this.setShape(x, y, w, h); + }, + + /** + * set new value to the rectangle shape + * @name setShape + * @memberOf me.Rect.prototype + * @function + * @param {Number} x position of the Rectangle + * @param {Number} y position of the Rectangle + * @param {Number|Array} w|points width of the rectangle, or an array of vector defining the rectangle + * @param {Number} [h] height of the rectangle, if a numeral width parameter is specified + * @return {me.Rect} this rectangle + */ + setShape: function setShape(x, y, w, h) { + var points = w; // assume w is an array by default + + if (arguments.length === 4) { + points = this.points; + points[0].set(0, 0); // 0, 0 + + points[1].set(w, 0); // 1, 0 + + points[2].set(w, h); // 1, 1 + + points[3].set(0, h); // 0, 1 + } + + this._super(me.Polygon, "setShape", [x, y, points]); // private properties to cache width & height + + + this._width = this.points[2].x; // w + + this._height = this.points[2].y; // h + + return this; + }, + + /** + * resize the rectangle + * @name resize + * @memberOf me.Rect.prototype + * @function + * @param {Number} w new width of the rectangle + * @param {Number} h new height of the rectangle + * @return {me.Rect} this rectangle + */ + resize: function resize(w, h) { + this.width = w; + this.height = h; + return this; + }, + + /** + * returns the bounding box for this shape, the smallest rectangle object completely containing this shape. + * @name getBounds + * @memberOf me.Rect.prototype + * @function + * @return {me.Rect} this shape bounding box Rectangle object + */ + getBounds: function getBounds() { + return this; + }, + + /** + * resize the rectangle to contain all the given points coordinates. + * @name setPoints + * @memberOf me.Rect.prototype + * @function + * @param {me.Vector2d[]} points array of vector defining a shape + * @return {me.Rect} this shape bounding box Rectangle object + */ + setPoints: function setPoints(points) { + var x = Infinity, + y = Infinity, + right = -Infinity, + bottom = -Infinity; + points.forEach(function (point) { + x = Math.min(x, point.x); + y = Math.min(y, point.y); + right = Math.max(right, point.x); + bottom = Math.max(bottom, point.y); + }); + this.setShape(x, y, right - x, bottom - y); + return this; + }, + + /** + * Computes the calculated collision polygon. + * This **must** be called if the `points` array is modified manually. + * @ignore + * @name recalc + * @memberOf me.Rect.prototype + * @function + */ + recalc: function recalc() { + this._super(me.Polygon, "recalc"); + + this._width = this.points[2].x; + this._height = this.points[2].y; + return this; + }, + + /** + * update the bounding box for this shape. + * @name updateBounds + * @memberOf me.Rect.prototype + * @function + * @return {me.Rect} this shape bounding box Rectangle object + */ + updateBounds: function updateBounds() { + return this; + }, + + /** + * clone this rectangle + * @name clone + * @memberOf me.Rect.prototype + * @function + * @return {me.Rect} new rectangle + */ + clone: function clone() { + return new me.Rect(this.pos.x, this.pos.y, this._width, this._height); + }, + + /** + * copy the position and size of the given rectangle into this one + * @name copy + * @memberOf me.Rect.prototype + * @function + * @param {me.Rect} rect Source rectangle + * @return {me.Rect} new rectangle + */ + copy: function copy(rect) { + return this.setShape(rect.pos.x, rect.pos.y, rect._width, rect._height); + }, + + /** + * translate the rect by the specified offset + * @name translate + * @memberOf me.Rect.prototype + * @function + * @param {Number} x x offset + * @param {Number} y y offset + * @return {me.Rect} this rectangle + */ + translate: function translate(x, y) { + this.pos.x += x; + this.pos.y += y; + return this; + }, + + /** + * translate the rect by the specified vector + * @name translateV + * @memberOf me.Rect.prototype + * @function + * @param {me.Vector2d} v vector offset + * @return {me.Rect} this rectangle + */ + translateV: function translateV(v) { + return this.translate(v.x, v.y); + }, + + /** + * merge this rectangle with another one + * @name union + * @memberOf me.Rect.prototype + * @function + * @param {me.Rect} rect other rectangle to union with + * @return {me.Rect} the union(ed) rectangle + */ + union: function union( + /** {me.Rect} */ + r) { + var x1 = Math.min(this.left, r.left); + var y1 = Math.min(this.top, r.top); + this.resize(Math.max(this.right, r.right) - x1, Math.max(this.bottom, r.bottom) - y1); + this.pos.set(x1, y1); + return this; + }, + + /** + * check if this rectangle is intersecting with the specified one + * @name overlaps + * @memberOf me.Rect.prototype + * @function + * @param {me.Rect} rect + * @return {boolean} true if overlaps + */ + overlaps: function overlaps(r) { + return this.left < r.right && r.left < this.right && this.top < r.bottom && r.top < this.bottom; + }, + + /** + * check if this rectangle contains the specified one + * @name contains + * @memberOf me.Rect.prototype + * @function + * @param {me.Rect} rect + * @return {boolean} true if contains + */ + contains: function contains(r) { + return r.left >= this.left && r.right <= this.right && r.top >= this.top && r.bottom <= this.bottom; + }, + + /** + * check if this rectangle contains the specified point + * @name containsPoint + * @memberOf me.Rect.prototype + * @function + * @param {Number} x x coordinate + * @param {Number} y y coordinate + * @return {boolean} true if contains + */ + containsPoint: function containsPoint(x, y) { + return x >= this.left && x <= this.right && y >= this.top && y <= this.bottom; + }, + + /** + * check if this rectangle is identical to the specified one + * @name equals + * @memberOf me.Rect.prototype + * @function + * @param {me.Rect} rect + * @return {boolean} true if equals + */ + equals: function equals(r) { + return r.left === this.left && r.right === this.right && r.top === this.top && r.bottom === this.bottom; + }, + + /** + * determines whether all coordinates of this rectangle are finite numbers. + * @name isFinite + * @memberOf me.Rect.prototype + * @function + * @return {boolean} false if all coordinates are positive or negative Infinity or NaN; otherwise, true. + */ + isFinite: function (_isFinite) { + function isFinite() { + return _isFinite.apply(this, arguments); + } + + isFinite.toString = function () { + return _isFinite.toString(); + }; + + return isFinite; + }(function () { + return isFinite(this.pos.x) && isFinite(this.pos.y) && isFinite(this._width) && isFinite(this._height); + }), + + /** + * Returns a polygon whose edges are the same as this box. + * @name toPolygon + * @memberOf me.Rect.prototype + * @function + * @return {me.Polygon} a new Polygon that represents this rectangle. + */ + toPolygon: function toPolygon() { + return new me.Polygon(this.pos.x, this.pos.y, this.points); + } + }); // redefine some properties to ease our life when getting the rectangle coordinates + + /** + * left coordinate of the Rectangle + * @public + * @type {Number} + * @name left + * @memberOf me.Rect + */ + + Object.defineProperty(me.Rect.prototype, "left", { + /** + * @ignore + */ + get: function get() { + return this.pos.x; + }, + configurable: true + }); + /** + * right coordinate of the Rectangle + * @public + * @type {Number} + * @name right + * @memberOf me.Rect + */ + + Object.defineProperty(me.Rect.prototype, "right", { + /** + * @ignore + */ + get: function get() { + var w = this._width; + return this.pos.x + w || w; + }, + configurable: true + }); + /** + * top coordinate of the Rectangle + * @public + * @type {Number} + * @name top + * @memberOf me.Rect + */ + + Object.defineProperty(me.Rect.prototype, "top", { + /** + * @ignore + */ + get: function get() { + return this.pos.y; + }, + configurable: true + }); + /** + * bottom coordinate of the Rectangle + * @public + * @type {Number} + * @name bottom + * @memberOf me.Rect + */ + + Object.defineProperty(me.Rect.prototype, "bottom", { + /** + * @ignore + */ + get: function get() { + var h = this._height; + return this.pos.y + h || h; + }, + configurable: true + }); + /** + * width of the Rectangle + * @public + * @type {Number} + * @name width + * @memberOf me.Rect + */ + + Object.defineProperty(me.Rect.prototype, "width", { + /** + * @ignore + */ + get: function get() { + return this._width; + }, + + /** + * @ignore + */ + set: function set(value) { + if (this._width !== value) { + this.points[1].x = this.points[2].x = value; // _width updated in recalc + + this.recalc(); + } + }, + configurable: true + }); + /** + * height of the Rectangle + * @public + * @type {Number} + * @name height + * @memberOf me.Rect + */ + + Object.defineProperty(me.Rect.prototype, "height", { + /** + * @ignore + */ + get: function get() { + return this._height; + }, + + /** + * @ignore + */ + set: function set(value) { + if (this._height !== value) { + this.points[2].y = this.points[3].y = value; // _height updated in recalc + + this.recalc(); + } + }, + configurable: true + }); + /** + * absolute center of this rectangle on the horizontal axis + * @public + * @type {Number} + * @name centerX + * @memberOf me.Rect + */ + + Object.defineProperty(me.Rect.prototype, "centerX", { + /** + * @ignore + */ + get: function get() { + return this.pos.x + this._width / 2; + }, + + /** + * @ignore + */ + set: function set(value) { + this.pos.x = value - this._width / 2; + }, + configurable: true + }); + /** + * absolute center of this rectangle on the vertical axis + * @public + * @type {Number} + * @name centerY + * @memberOf me.Rect + */ + + Object.defineProperty(me.Rect.prototype, "centerY", { + /** + * @ignore + */ + get: function get() { + return this.pos.y + this._height / 2; + }, + + /** + * @ignore + */ + set: function set(value) { + this.pos.y = value - this._height / 2; + }, + configurable: true + }); + })(); + + (function () { + /** + * a line segment Object.
+ * @class + * @extends me.Polygon + * @memberOf me + * @constructor + * @param {Number} x origin point of the Line + * @param {Number} y origin point of the Line + * @param {me.Vector2d[]} points array of vectors defining the Line + */ + me.Line = me.Polygon.extend({ + /** + * check if this line segment contains the specified point + * @name containsPointV + * @memberOf me.Line.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 line segment contains the specified point + * @name containsPoint + * @memberOf me.Line.prototype + * @function + * @param {Number} x x coordinate + * @param {Number} y y coordinate + * @return {boolean} true if contains + */ + containsPoint: function containsPoint(x, y) { + // translate the given coordinates, + // rather than creating temp translated vectors + x -= this.pos.x; // Cx + + y -= this.pos.y; // Cy + + var start = this.points[0]; // Ax/Ay + + var end = this.points[1]; // Bx/By + //(Cy - Ay) * (Bx - Ax) = (By - Ay) * (Cx - Ax) + + return (y - start.y) * (end.x - start.x) === (end.y - start.y) * (x - start.x); + }, + + /** + * Computes the calculated collision edges and normals. + * This **must** be called if the `points` array, `angle`, or `offset` is modified manually. + * @name recalc + * @memberOf me.Line.prototype + * @function + */ + recalc: function recalc() { + var edges = this.edges; + var normals = this.normals; // Copy the original points array and apply the offset/angle + + var points = this.points; + + if (points.length !== 2) { + throw new Error("Requires exactly 2 points"); + } // Calculate the edges/normals + + + if (edges[0] === undefined) { + edges[0] = new me.Vector2d(); + } + + edges[0].copy(points[1]).sub(points[0]); + + if (normals[0] === undefined) { + normals[0] = new me.Vector2d(); + } + + normals[0].copy(edges[0]).perp().normalize(); + return this; + }, + + /** + * clone this line segment + * @name clone + * @memberOf me.Line.prototype + * @function + * @return {me.Line} new Line + */ + clone: function clone() { + var copy = []; + this.points.forEach(function (point) { + copy.push(point.clone()); + }); + return new me.Line(this.pos.x, this.pos.y, copy); + } + }); + })(); + + (function () { + /** + * a Generic Body Object with some physic properties and behavior functionality
+ The body object is offten attached as a member of an Entity. The Body object can handle movements of the parent with + the body.update call. It important to know that when body.update is called there are several things that happen related to + the movement and positioning of the parent entity (assuming its an Entity). 1) The force/gravity/friction parameters are used + to calcuate a new velocity and 2) the parent position is updated by adding this to the parent.pos (position me.Vector2d) + value. Thus Affecting the movement of the parent. Look at the source code for /src/physics/body.js:update (me.Body.update) for + a better understanding. + * @class + * @extends me.Rect + * @memberOf me + * @constructor + * @param {me.Renderable} ancestor the parent object this body is attached to + * @param {me.Rect[]|me.Polygon[]|me.Line[]|me.Ellipse[]} [shapes] the initial list of shapes + * @param {Function} [onBodyUpdate] callback for when the body is updated (e.g. add/remove shapes) + */ + me.Body = me.Rect.extend({ + /** + * @ignore + */ + init: function init(parent, shapes, onBodyUpdate) { + /** + * a reference to the parent object that contains this body, + * or undefined if it has not been added to one. + * @public + * @type me.Renderable + * @default undefined + * @name me.Body#ancestor + */ + this.ancestor = parent; + /** + * The collision shapes of the body
+ * @ignore + * @type {me.Polygon[]|me.Line[]|me.Ellipse[]} + * @name shapes + * @memberOf me.Body + */ + + if (typeof this.shapes === "undefined") { + this.shapes = []; + } + /** + * The body collision mask, that defines what should collide with what.
+ * (by default will collide with all entities) + * @ignore + * @type Number + * @default me.collision.types.ALL_OBJECT + * @name collisionMask + * @see me.collision.types + * @memberOf me.Body + */ + + + this.collisionMask = me.collision.types.ALL_OBJECT; + /** + * define the collision type of the body for collision filtering + * @public + * @type Number + * @default me.collision.types.ENEMY_OBJECT + * @name collisionType + * @see me.collision.types + * @memberOf me.Body + * @example + * // set the body collision type + * myEntity.body.collisionType = me.collision.types.PLAYER_OBJECT; + */ + + this.collisionType = me.collision.types.ENEMY_OBJECT; + /** + * body velocity
+ * + * @public + * @type me.Vector2d + * @default <0,0> + * @name vel + * @memberOf me.Body + */ + + if (typeof this.vel === "undefined") { + this.vel = new me.Vector2d(); + } + + this.vel.set(0, 0); + /** + * body acceleration
+ * Not fully implemented yet. At this time accel is used to set the MaximumVelocity allowed. + * @public + * @type me.Vector2d + * @default <0,0> + * @name accel + * @deprecated + * @see me.Body.force + * @memberOf me.Body + */ + + if (typeof this.accel === "undefined") { + this.accel = new me.Vector2d(); + } + + this.accel.set(0, 0); + /** + * body force or acceleration (automatically) applied to the body. + * when defining a force, user should also define a max velocity + * @public + * @type me.Vector2d + * @default <0,0> + * @name force + * @see me.Body.setMaxVelocity + * @memberOf me.Body + * @example + * init: function () { + * // define a default maximum acceleration, initial force and friction + * this.body.force.set(0, 0); + * this.body.friction.set(0.4, 0); + * this.body.setMaxVelocity(3, 15); + * }, + * + * // apply a postive or negative force when pressing left of right key + * update : function (dt) { + * if (me.input.isKeyPressed("left")) { + * this.body.force.x = -this.body.maxVel.x; + * } else if (me.input.isKeyPressed("right")) { + * this.body.force.x = this.body.maxVel.x; + * } else { + * this.body.force.x = 0; + * } + * } + */ + + if (typeof this.force === "undefined") { + this.force = new me.Vector2d(); + } + + this.force.set(0, 0); + /** + * body friction + * @public + * @type me.Vector2d + * @default <0,0> + * @name friction + * @memberOf me.Body + */ + + if (typeof this.friction === "undefined") { + this.friction = new me.Vector2d(); + } + + this.friction.set(0, 0); + /** + * the body bouciness level when colliding with other solid bodies : + * a value of 0 will not bounce, a value of 1 will fully rebound. + * @public + * @type {Number} + * @default 0 + * @name bounce + * @memberOf me.Body + */ + + this.bounce = 0; + /** + * the body mass + * @public + * @type {Number} + * @default 1 + * @name mass + * @memberOf me.Body + */ + + this.mass = 1; + /** + * max velocity (to limit body velocity) + * @public + * @type me.Vector2d + * @default <490,490> + * @name maxVel + * @memberOf me.Body + */ + + if (typeof this.maxVel === "undefined") { + this.maxVel = new me.Vector2d(); + } // cap by default to half the default gravity force + + + this.maxVel.set(490, 490); + /** + * Default gravity value for this body. + * To be set to to < 0, 0 > for RPG, shooter, etc...
+ * Note: y axis gravity can also globally be defined through me.sys.gravity + * @public + * @see me.sys.gravity + * @type me.Vector2d + * @default <0,0.98> + * @default + * @name gravity + * @memberOf me.Body + */ + + if (typeof this.gravity === "undefined") { + this.gravity = new me.Vector2d(); + } + + this.gravity.set(0, typeof me.sys.gravity === "number" ? me.sys.gravity : 0.98); + /** + * falling state of the body
+ * true if the object is falling
+ * false if the object is standing on something
+ * @readonly + * @public + * @type Boolean + * @default false + * @name falling + * @memberOf me.Body + */ + + this.falling = false; + /** + * jumping state of the body
+ * equal true if the body is jumping
+ * @readonly + * @public + * @type Boolean + * @default false + * @name jumping + * @memberOf me.Body + */ + + this.jumping = false; // call the super constructor + + this._super(me.Rect, "init", [0, 0, this.ancestor.width, this.ancestor.height]); + + if (typeof onBodyUpdate === "function") { + this.onBodyUpdate = onBodyUpdate; + } // parses the given shapes array and add them + + + if (Array.isArray(shapes)) { + for (var s = 0; s < shapes.length; s++) { + this.addShape(shapes[s], true); + } + + this.updateBounds(); + } + }, + + /** + * add a collision shape to this body
+ * (note: me.Rect objects will be converted to me.Polygon before being added) + * @name addShape + * @memberOf me.Body + * @public + * @function + * @param {me.Rect|me.Polygon|me.Line|me.Ellipse} shape a shape object + * @param {Boolean} batchInsert if true the body bounds won't be updated after adding a shape + * @return {Number} the shape array length + */ + addShape: function addShape(shape, batchInsert) { + if (shape instanceof me.Rect) { + this.shapes.push(shape.toPolygon()); + } else { + // else polygon or circle + this.shapes.push(shape); + } + + if (batchInsert !== true) { + // update the body bounds to take in account the added shape + this.updateBounds(); + } // return the length of the shape list + + + return this.shapes.length; + }, + + /** + * add collision mesh based on a given Physics Editor JSON object. + * (this will also apply any physic properties defined in the given JSON file) + * @name addShapesFromJSON + * @memberOf me.Body + * @public + * @function + * @param {Object} json a JSON object as exported from the a Physics Editor tool + * @param {String} id the shape identifier within the given the json object + * @param {String} [scale=1] the desired scale of the body (physic-body-editor only) + * @see https://www.codeandweb.com/physicseditor + * @return {Number} the shape array length + * @example + * this.body.addShapesFromJSON(me.loader.getJSON("shapesdef1"), settings.banana); + */ + addShapesFromJSON: function addShapesFromJSON(json, id, scale) { + var data; + scale = scale || 1; // identify the json format + + if (typeof json.rigidBodies === "undefined") { + // Physic Editor Format (https://www.codeandweb.com/physicseditor) + data = json[id]; + + if (typeof data === "undefined") { + throw new Error("Identifier (" + id + ") undefined for the given PhysicsEditor JSON object)"); + } + + if (data.length) { + // go through all shapes and add them to the body + for (var i = 0; i < data.length; i++) { + var points = []; + + for (var s = 0; s < data[i].shape.length; s += 2) { + points.push(new me.Vector2d(data[i].shape[s], data[i].shape[s + 1])); + } + + this.addShape(new me.Polygon(0, 0, points), true); + } // apply density, friction and bounce properties from the first shape + // Note : how to manage different mass or friction for all different shapes? + + + this.mass = data[0].density || 0; + this.friction.set(data[0].friction || 0, data[0].friction || 0); + this.bounce = data[0].bounce || 0; + } + } else { + // Physic Body Editor Format (http://www.aurelienribon.com/blog/projects/physics-body-editor/) + json.rigidBodies.forEach(function (shape) { + if (shape.name === id) { + data = shape; // how to stop a forEach loop? + } + }); + + if (typeof data === "undefined") { + throw new Error("Identifier (" + id + ") undefined for the given PhysicsEditor JSON object)"); + } // shapes origin point + // top-left origin in the editor is (0,1) + + + this.pos.set(data.origin.x, 1.0 - data.origin.y).scale(scale); + var self = this; // parse all polygons + + data.polygons.forEach(function (poly) { + var points = []; + poly.forEach(function (point) { + // top-left origin in the editor is (0,1) + points.push(new me.Vector2d(point.x, 1.0 - point.y).scale(scale)); + }); + self.addShape(new me.Polygon(0, 0, points), true); + }); // parse all circles + + data.circles.forEach(function (circle) { + self.addShape(new me.Ellipse(circle.cx * scale, (1.0 - circle.cy) * scale, circle.r * 2 * scale, circle.r * 2 * scale), true); + }); + } // update the body bounds to take in account the added shapes + + + this.updateBounds(); // return the length of the shape list + + return this.shapes.length; + }, + + /** + * return the collision shape at the given index + * @name getShape + * @memberOf me.Body + * @public + * @function + * @param {Number} [index=0] the shape object at the specified index + * @return {me.Polygon|me.Line|me.Ellipse} shape a shape object if defined + */ + getShape: function getShape(index) { + return this.shapes[index || 0]; + }, + + /** + * remove the specified shape from the body shape list + * @name removeShape + * @memberOf me.Body + * @public + * @function + * @param {me.Polygon|me.Line|me.Ellipse} shape a shape object + * @return {Number} the shape array length + */ + removeShape: function removeShape(shape) { + me.utils.array.remove(this.shapes, shape); // update the body bounds to take in account the removed shape + + this.updateBounds(); // return the length of the shape list + + return this.shapes.length; + }, + + /** + * remove the shape at the given index from the body shape list + * @name removeShapeAt + * @memberOf me.Body + * @public + * @function + * @param {Number} index the shape object at the specified index + * @return {Number} the shape array length + */ + removeShapeAt: function removeShapeAt(index) { + return this.removeShape(this.getShape(index)); + }, + + /** + * By default all entities are able to collide with all other entities,
+ * but it's also possible to specificy 'collision filters' to provide a finer
+ * control over which entities can collide with each other. + * @name setCollisionMask + * @memberOf me.Body + * @public + * @function + * @see me.collision.types + * @param {Number} bitmask the collision mask + * @example + * // filter collision detection with collision shapes, enemies and collectables + * myEntity.body.setCollisionMask(me.collision.types.WORLD_SHAPE | me.collision.types.ENEMY_OBJECT | me.collision.types.COLLECTABLE_OBJECT); + * ... + * // disable collision detection with all other objects + * myEntity.body.setCollisionMask(me.collision.types.NO_OBJECT); + */ + setCollisionMask: function setCollisionMask(bitmask) { + this.collisionMask = bitmask; + }, + + /** + * the built-in function to solve the collision response + * @protected + * @name respondToCollision + * @memberOf me.Body + * @function + * @param {me.collision.ResponseObject} response the collision response object + */ + respondToCollision: function respondToCollision(response) { + // the overlap vector + var overlap = response.overlapV; // FIXME: Respond proportionally to object mass + // Move out of the other object shape + + this.ancestor.pos.sub(overlap); // adjust velocity + + if (overlap.x !== 0) { + this.vel.x = ~~(0.5 + this.vel.x - overlap.x) || 0; + + if (this.bounce > 0) { + this.vel.x *= -this.bounce; + } + } + + if (overlap.y !== 0) { + this.vel.y = ~~(0.5 + this.vel.y - overlap.y) || 0; + + if (this.bounce > 0) { + this.vel.y *= -this.bounce; + } // cancel the falling an jumping flags if necessary + + + var dir = Math.sign(this.gravity.y) || 1; + this.falling = overlap.y >= dir; + this.jumping = overlap.y <= -dir; + } + }, + + /** + * update the body bounding rect (private) + * the body rect size is here used to cache the total bounding rect + * @private + * @name updateBounds + * @memberOf me.Body + * @function + */ + updateBounds: function updateBounds() { + if (this.shapes.length > 0) { + // reset the rect with default values + var _bounds = this.shapes[0].getBounds(); + + this.pos.setV(_bounds.pos); + this.resize(_bounds.width, _bounds.height); + + for (var i = 1; i < this.shapes.length; i++) { + this.union(this.shapes[i].getBounds()); + } + } // trigger the onBodyChange + + + if (typeof this.onBodyUpdate === "function") { + this.onBodyUpdate(this); + } + + return this; + }, + + /** + * Sets accel to Velocity if x or y is not 0. Net effect is to set the maxVel.x/y to the passed values for x/y
+ * note: This does not set the vel member of the body object. This is identical to the setMaxVelocity call except that the + * accel property is updated to match the passed x and y. + * setMaxVelocity if needed
+ * @name setVelocity + * @memberOf me.Body + * @function + * @param {Number} x velocity on x axis + * @param {Number} y velocity on y axis + * @protected + * @deprecated + * @see me.Body.force + */ + setVelocity: function setVelocity(x, y) { + this.accel.x = x !== 0 ? x : this.accel.x; + this.accel.y = y !== 0 ? y : this.accel.y; // limit by default to the same max value + + this.setMaxVelocity(x, y); + }, + + /** + * cap the body velocity (body.maxVel property) to the specified value
+ * @name setMaxVelocity + * @memberOf me.Body + * @function + * @param {Number} x max velocity on x axis + * @param {Number} y max velocity on y axis + * @protected + */ + setMaxVelocity: function setMaxVelocity(x, y) { + this.maxVel.x = x; + this.maxVel.y = y; + }, + + /** + * set the body default friction + * @name setFriction + * @memberOf me.Body + * @function + * @param {Number} x horizontal friction + * @param {Number} y vertical friction + * @protected + */ + setFriction: function setFriction(x, y) { + this.friction.x = x || 0; + this.friction.y = y || 0; + }, + + /** + * apply friction to a vector + * @ignore + */ + applyFriction: function applyFriction(vel) { + var fx = this.friction.x * me.timer.tick, + nx = vel.x + fx, + x = vel.x - fx, + fy = this.friction.y * me.timer.tick, + ny = vel.y + fy, + y = vel.y - fy; + vel.x = nx < 0 ? nx : x > 0 ? x : 0; + vel.y = ny < 0 ? ny : y > 0 ? y : 0; + }, + + /** + * compute the new velocity value + * @ignore + */ + computeVelocity: function computeVelocity(vel) { + // apply fore if defined + if (this.force.x) { + vel.x += this.force.x * me.timer.tick; + } + + if (this.force.y) { + vel.y += this.force.y * me.timer.tick; + } // apply friction + + + if (this.friction.x || this.friction.y) { + this.applyFriction(vel); + } // apply gravity if defined + + + if (this.gravity.y) { + vel.x += this.gravity.x * this.mass * me.timer.tick; + } + + if (this.gravity.y) { + vel.y += this.gravity.y * this.mass * me.timer.tick; // check if falling / jumping + + this.falling = vel.y * Math.sign(this.gravity.y) > 0; + this.jumping = this.falling ? false : this.jumping; + } // cap velocity + + + if (vel.y !== 0) { + vel.y = me.Math.clamp(vel.y, -this.maxVel.y, this.maxVel.y); + } + + if (vel.x !== 0) { + vel.x = me.Math.clamp(vel.x, -this.maxVel.x, this.maxVel.x); + } + }, + + /** + * Updates the parent's position as well as computes the new body's velocity based + * on the values of force/friction/gravity. Velocity chages are proportional to the + * me.timer.tick value (which can be used to scale velocities). The approach to moving the + * parent Entity is to compute new values of the Body.vel property then add them to + * the parent.pos value thus changing the postion the amount of Body.vel each time the + * update call is made.
+ * Updates to Body.vel are bounded by maxVel (which defaults to viewport size if not set)
+ * + * In addition, when the gravity calcuation is made, if the Body.vel.y > 0 then the Body.falling + * property is set to true and Body.jumping is set to !Body.falling. + * + * At this time a call to Body.Update does not call the onBodyUpdate callback that is listed in the init: function. + * @name update + * @memberOf me.Body + * @function + * @return {boolean} true if resulting velocity is different than 0 + * @see source code for me.Body.computeVelocity (private member) + */ + update: function update() + /* dt */ + { + // update the velocity + this.computeVelocity(this.vel); // update the body ancestor position + + this.ancestor.pos.add(this.vel); // returns true if vel is different from 0 + + return this.vel.x !== 0 || this.vel.y !== 0; + }, + + /** + * Destroy function
+ * @ignore + */ + destroy: function destroy() { + this.onBodyUpdate = undefined; + this.ancestor = undefined; + this.shapes.length = 0; + } + }); + })(); + + /* + * A QuadTree implementation in JavaScript, a 2d spatial subdivision algorithm. + * Based on the QuadTree Library by Timo Hausmann and released under the MIT license + * https://github.com/timohausmann/quadtree-js/ + **/ + (function () { + /** + * a pool of `QuadTree` objects + */ + var QT_ARRAY = []; + /** + * will pop a quadtree object from the array + * or create a new one if the array is empty + */ + + var QT_ARRAY_POP = function QT_ARRAY_POP(bounds, max_objects, max_levels, level) { + if (QT_ARRAY.length > 0) { + var _qt = QT_ARRAY.pop(); + + _qt.bounds = bounds; + _qt.max_objects = max_objects || 4; + _qt.max_levels = max_levels || 4; + _qt.level = level || 0; + return _qt; + } else { + return new me.QuadTree(bounds, max_objects, max_levels, level); + } + }; + /** + * Push back a quadtree back into the array + */ + + + var QT_ARRAY_PUSH = function QT_ARRAY_PUSH(qt) { + QT_ARRAY.push(qt); + }; + /** + * a temporary vector object to be reused + */ + + + var QT_VECTOR = new me.Vector2d(); + /** + * Quadtree Constructor
+ * note: the global quadtree instance is available through `me.collision.quadTree` + * @class + * @name QuadTree + * @extends Object + * @memberOf me + * @constructor + * @see me.collision.quadTree + * @param {me.Rect} bounds bounds of the node + * @param {Number} [max_objects=4] max objects a node can hold before splitting into 4 subnodes + * @param {Number} [max_levels=4] total max levels inside root Quadtree + * @param {Number} [level] deepth level, required for subnodes + */ + + function Quadtree(bounds, max_objects, max_levels, level) { + this.max_objects = max_objects || 4; + this.max_levels = max_levels || 4; + this.level = level || 0; + this.bounds = bounds; + this.objects = []; + this.nodes = []; + } + /* + * Split the node into 4 subnodes + */ + + + Quadtree.prototype.split = function () { + var nextLevel = this.level + 1, + subWidth = ~~(0.5 + this.bounds.width / 2), + subHeight = ~~(0.5 + this.bounds.height / 2), + x = ~~(0.5 + this.bounds.pos.x), + y = ~~(0.5 + this.bounds.pos.y); //top right node + + this.nodes[0] = QT_ARRAY_POP({ + pos: { + x: x + subWidth, + y: y + }, + width: subWidth, + height: subHeight + }, this.max_objects, this.max_levels, nextLevel); //top left node + + this.nodes[1] = QT_ARRAY_POP({ + pos: { + x: x, + y: y + }, + width: subWidth, + height: subHeight + }, this.max_objects, this.max_levels, nextLevel); //bottom left node + + this.nodes[2] = QT_ARRAY_POP({ + pos: { + x: x, + y: y + subHeight + }, + width: subWidth, + height: subHeight + }, this.max_objects, this.max_levels, nextLevel); //bottom right node + + this.nodes[3] = QT_ARRAY_POP({ + pos: { + x: x + subWidth, + y: y + subHeight + }, + width: subWidth, + height: subHeight + }, this.max_objects, this.max_levels, nextLevel); + }; + /* + * Determine which node the object belongs to + * @param {me.Rect} rect bounds of the area to be checked + * @return Integer index of the subnode (0-3), or -1 if rect cannot completely fit within a subnode and is part of the parent node + */ + + + Quadtree.prototype.getIndex = function (item) { + var rect = item.getBounds(), + pos = rect.pos; // use world coordinates for floating items + + if (item.floating || item.ancestor && item.ancestor.floating) { + pos = me.game.viewport.localToWorld(pos.x, pos.y, QT_VECTOR); + } + + var index = -1, + rx = pos.x, + ry = pos.y, + rw = rect.width, + rh = rect.height, + verticalMidpoint = this.bounds.pos.x + this.bounds.width / 2, + horizontalMidpoint = this.bounds.pos.y + this.bounds.height / 2, + //rect can completely fit within the top quadrants + topQuadrant = ry < horizontalMidpoint && ry + rh < horizontalMidpoint, + //rect can completely fit within the bottom quadrants + bottomQuadrant = ry > horizontalMidpoint; //rect can completely fit within the left quadrants + + if (rx < verticalMidpoint && rx + rw < verticalMidpoint) { + if (topQuadrant) { + index = 1; + } else if (bottomQuadrant) { + index = 2; + } + } else if (rx > verticalMidpoint) { + //rect can completely fit within the right quadrants + if (topQuadrant) { + index = 0; + } else if (bottomQuadrant) { + index = 3; + } + } + + return index; + }; + /** + * Insert the given object container into the node. + * @name insertContainer + * @memberOf me.QuadTree + * @function + * @param {me.Container} container group of objects to be added + */ + + + Quadtree.prototype.insertContainer = function (container) { + for (var i = container.children.length, child; i--, child = container.children[i];) { + if (child.isKinematic !== true) { + if (child instanceof me.Container) { + if (child.name !== "rootContainer") { + this.insert(child); + } // recursivly insert all childs + + + this.insertContainer(child); + } else { + // only insert object with a bounding box + // Probably redundant with `isKinematic` + if (typeof child.getBounds === "function") { + this.insert(child); + } + } + } + } + }; + /** + * Insert the given object into the node. If the node + * exceeds the capacity, it will split and add all + * objects to their corresponding subnodes. + * @name insert + * @memberOf me.QuadTree + * @function + * @param {Object} item object to be added + */ + + + Quadtree.prototype.insert = function (item) { + var index = -1; //if we have subnodes ... + + if (this.nodes.length > 0) { + index = this.getIndex(item); + + if (index !== -1) { + this.nodes[index].insert(item); + return; + } + } + + this.objects.push(item); + + if (this.objects.length > this.max_objects && this.level < this.max_levels) { + //split if we don't already have subnodes + if (this.nodes.length === 0) { + this.split(); + } + + var i = 0; //add all objects to there corresponding subnodes + + while (i < this.objects.length) { + index = this.getIndex(this.objects[i]); + + if (index !== -1) { + this.nodes[index].insert(this.objects.splice(i, 1)[0]); + } else { + i = i + 1; + } + } + } + }; + /** + * Return all objects that could collide with the given object + * @name retrieve + * @memberOf me.QuadTree + * @function + * @param {Object} object object to be checked against + * @param {Object} [function] a sorting function for the returned array + * @return {Object[]} array with all detected objects + */ + + + Quadtree.prototype.retrieve = function (item, fn) { + var returnObjects = this.objects; //if we have subnodes ... + + if (this.nodes.length > 0) { + var index = this.getIndex(item); //if rect fits into a subnode .. + + if (index !== -1) { + returnObjects = returnObjects.concat(this.nodes[index].retrieve(item)); + } else { + //if rect does not fit into a subnode, check it against all subnodes + for (var i = 0; i < this.nodes.length; i = i + 1) { + returnObjects = returnObjects.concat(this.nodes[i].retrieve(item)); + } + } + } + + if (typeof fn === "function") { + returnObjects.sort(fn); + } + + return returnObjects; + }; + /** + * Remove the given item from the quadtree. + * (this function won't recalculate the impacted node) + * @name remove + * @memberOf me.QuadTree + * @function + * @param {Object} object object to be removed + * @return true if the item was found and removed. + */ + + + Quadtree.prototype.remove = function (item) { + var found = false; + + if (typeof item.getBounds === "undefined") { + // ignore object that cannot be added in the first place + return false; + } //if we have subnodes ... + + + if (this.nodes.length > 0) { + // determine to which node the item belongs to + var index = this.getIndex(item); + + if (index !== -1) { + found = me.utils.array.remove(this.nodes[index], item); // trim node if empty + + if (found && this.nodes[index].isPrunable()) { + this.nodes.splice(index, 1); + } + } + } + + if (found === false) { + // try and remove the item from the list of items in this node + if (this.objects.indexOf(item) !== -1) { + me.utils.array.remove(this.objects, item); + found = true; + } + } + + return found; + }; + /** + * return true if the node is prunable + * @name isPrunable + * @memberOf me.QuadTree + * @function + * @return true if the node is prunable + */ + + + Quadtree.prototype.isPrunable = function () { + return !(this.hasChildren() || this.objects.length > 0); + }; + /** + * return true if the node has any children + * @name hasChildren + * @memberOf me.QuadTree + * @function + * @return true if the node has any children + */ + + + Quadtree.prototype.hasChildren = function () { + for (var i = 0; i < this.nodes.length; i = i + 1) { + var subnode = this.nodes[i]; + + if (subnode.length > 0 || subnode.objects.length > 0) { + return true; + } + } + + return false; + }; + /** + * clear the quadtree + * @name clear + * @memberOf me.QuadTree + * @function + */ + + + Quadtree.prototype.clear = function (bounds) { + this.objects.length = 0; + + for (var i = 0; i < this.nodes.length; i = i + 1) { + this.nodes[i].clear(); // recycle the quadTree object + + QT_ARRAY_PUSH(this.nodes[i]); + } // empty the array + + + this.nodes.length = 0; // resize the root bounds if required + + if (typeof bounds !== "undefined") { + this.bounds.setShape(bounds.pos.x, bounds.pos.y, bounds.width, bounds.height); + } + }; //make Quadtree available in the me namespace + + + me.QuadTree = Quadtree; + })(); + + /* + * Separating Axis Theorem implementation, based on the SAT.js library by Jim Riecken + * Available under the MIT License - https://github.com/jriecken/sat-js + */ + (function () { + /** + * Constants for Vornoi regions + * @ignore + */ + var LEFT_VORNOI_REGION = -1; + /** + * Constants for Vornoi regions + * @ignore + */ + + var MIDDLE_VORNOI_REGION = 0; + /** + * Constants for Vornoi regions + * @ignore + */ + + var RIGHT_VORNOI_REGION = 1; + /** + * A pool of `Vector` objects that are used in calculations to avoid allocating memory. + * @type {Array.} + */ + + var T_VECTORS = []; + + for (var v = 0; v < 10; v++) { + T_VECTORS.push(new me.Vector2d()); + } + /** + * A pool of arrays of numbers used in calculations to avoid allocating memory. + * @type {Array.>} + */ + + + var T_ARRAYS = []; + + for (var a = 0; a < 5; a++) { + T_ARRAYS.push([]); + } // a dummy entity object when using Line for raycasting + + + var dummyEntity = { + pos: new me.Vector2d(0, 0), + ancestor: { + _absPos: new me.Vector2d(0, 0) + } + }; + /** + * Flattens the specified array of points onto a unit vector axis, + * resulting in a one dimensional range of the minimum and + * maximum value on that axis. + * @param {Array.} points The points to flatten. + * @param {Vector} normal The unit vector axis to flatten on. + * @param {Array.} result An array. After calling this function, + * result[0] will be the minimum value, + * result[1] will be the maximum value. + */ + + function flattenPointsOn(points, normal, result) { + var min = Number.MAX_VALUE; + var max = -Number.MAX_VALUE; + var len = points.length; + + for (var i = 0; i < len; i++) { + // The magnitude of the projection of the point onto the normal + var dot = points[i].dotProduct(normal); + + if (dot < min) { + min = dot; + } + + if (dot > max) { + max = dot; + } + } + + result[0] = min; + result[1] = max; + } + /** + * Check whether two convex polygons are separated by the specified + * axis (must be a unit vector). + * @param {Vector} aPos The position of the first polygon. + * @param {Vector} bPos The position of the second polygon. + * @param {Array.} aPoints The points in the first polygon. + * @param {Array.} bPoints The points in the second polygon. + * @param {Vector} axis The axis (unit sized) to test against. The points of both polygons + * will be projected onto this axis. + * @param {Response=} response A Response object (optional) which will be populated + * if the axis is not a separating axis. + * @return {boolean} true if it is a separating axis, false otherwise. If false, + * and a response is passed in, information about how much overlap and + * the direction of the overlap will be populated. + */ + + + function isSeparatingAxis(aPos, bPos, aPoints, bPoints, axis, response) { + var rangeA = T_ARRAYS.pop(); + var rangeB = T_ARRAYS.pop(); // The magnitude of the offset between the two polygons + + var offsetV = T_VECTORS.pop().copy(bPos).sub(aPos); + var projectedOffset = offsetV.dotProduct(axis); // Project the polygons onto the axis. + + flattenPointsOn(aPoints, axis, rangeA); + flattenPointsOn(bPoints, axis, rangeB); // Move B's range to its position relative to A. + + rangeB[0] += projectedOffset; + rangeB[1] += projectedOffset; // Check if there is a gap. If there is, this is a separating axis and we can stop + + if (rangeA[0] > rangeB[1] || rangeB[0] > rangeA[1]) { + T_VECTORS.push(offsetV); + T_ARRAYS.push(rangeA); + T_ARRAYS.push(rangeB); + return true; + } // This is not a separating axis. If we're calculating a response, calculate the overlap. + + + if (response) { + var overlap = 0; // A starts further left than B + + if (rangeA[0] < rangeB[0]) { + response.aInB = false; // A ends before B does. We have to pull A out of B + + if (rangeA[1] < rangeB[1]) { + overlap = rangeA[1] - rangeB[0]; + response.bInA = false; // B is fully inside A. Pick the shortest way out. + } else { + var option1 = rangeA[1] - rangeB[0]; + var option2 = rangeB[1] - rangeA[0]; + overlap = option1 < option2 ? option1 : -option2; + } // B starts further left than A + + } else { + response.bInA = false; // B ends before A ends. We have to push A out of B + + if (rangeA[1] > rangeB[1]) { + overlap = rangeA[0] - rangeB[1]; + response.aInB = false; // A is fully inside B. Pick the shortest way out. + } else { + var option11 = rangeA[1] - rangeB[0]; + var option22 = rangeB[1] - rangeA[0]; + overlap = option11 < option22 ? option11 : -option22; + } + } // If this is the smallest amount of overlap we've seen so far, set it as the minimum overlap. + + + var absOverlap = Math.abs(overlap); + + if (absOverlap < response.overlap) { + response.overlap = absOverlap; + response.overlapN.copy(axis); + + if (overlap < 0) { + response.overlapN.negateSelf(); + } + } + } + + T_VECTORS.push(offsetV); + T_ARRAYS.push(rangeA); + T_ARRAYS.push(rangeB); + return false; + } + /** + * Calculates which Vornoi region a point is on a line segment.
+ * It is assumed that both the line and the point are relative to `(0,0)`
+ *
+      *             |       (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.
+ * Based on the Separating Axis Theorem and supports detecting collisions between simple Axis-Aligned Boxes, convex polygons and circles based shapes. + * @namespace me.collision + * @memberOf me + */ + + + me.collision = function () { + // hold public stuff in our singleton + var api = {}; + /* + * PUBLIC STUFF + */ + + /** + * the world quadtree used for the collision broadphase + * @name quadTree + * @memberOf me.collision + * @public + * @type {me.QuadTree} + */ + + api.quadTree = null; + /** + * The maximum number of levels that the quadtree will create. + * @name maxDepth + * @memberOf me.collision + * @public + * @type {Number} + * @default 4 + * @see me.collision.quadTree + * + */ + + api.maxDepth = 4; + /** + * The maximum number of children that a quadtree node can contain before it is split into sub-nodes. + * @name maxChildren + * @memberOf me.collision + * @public + * @type {Number} + * @default 8 + * @see me.collision.quadTree + */ + + api.maxChildren = 8; + /** + * bounds of the physic world. + * @name bounds + * @memberOf me.collision + * @public + * @type {me.Rect} + */ + + api.bounds = null; + /** + * Enum for collision type values. + * @property NO_OBJECT to disable collision check + * @property PLAYER_OBJECT + * @property NPC_OBJECT + * @property ENEMY_OBJECT + * @property COLLECTABLE_OBJECT + * @property ACTION_OBJECT e.g. doors + * @property PROJECTILE_OBJECT e.g. missiles + * @property WORLD_SHAPE e.g. walls; for map collision shapes + * @property USER user-defined collision types (see example) + * @property ALL_OBJECT all of the above (including user-defined types) + * @readonly + * @enum {Number} + * @name types + * @memberOf me.collision + * @see me.body.setCollisionMask + * @see me.body.collisionType + * @example + * // set the entity body collision type + * myEntity.body.collisionType = me.collision.types.PLAYER_OBJECT; + * + * // filter collision detection with collision shapes, enemies and collectables + * myEntity.body.setCollisionMask( + * me.collision.types.WORLD_SHAPE | + * me.collision.types.ENEMY_OBJECT | + * me.collision.types.COLLECTABLE_OBJECT + * ); + * + * // User-defined collision types are defined using BITWISE LEFT-SHIFT: + * game.collisionTypes = { + * LOCKED_DOOR : me.collision.types.USER << 0, + * OPEN_DOOR : me.collision.types.USER << 1, + * LOOT : me.collision.types.USER << 2, + * }; + * + * // Set collision type for a door entity + * myDoorEntity.body.collisionType = game.collisionTypes.LOCKED_DOOR; + * + * // Set collision mask for the player entity, so it collides with locked doors and loot + * myPlayerEntity.body.setCollisionMask( + * me.collision.types.ENEMY_OBJECT | + * me.collision.types.WORLD_SHAPE | + * game.collisionTypes.LOCKED_DOOR | + * game.collisionTypes.LOOT + * ); + */ + + api.types = { + /** to disable collision check */ + NO_OBJECT: 0, + PLAYER_OBJECT: 1 << 0, + NPC_OBJECT: 1 << 1, + ENEMY_OBJECT: 1 << 2, + COLLECTABLE_OBJECT: 1 << 3, + ACTION_OBJECT: 1 << 4, + // door, etc... + PROJECTILE_OBJECT: 1 << 5, + // missiles, etc... + WORLD_SHAPE: 1 << 6, + // walls, etc... + USER: 1 << 7, + // user-defined types start here... + ALL_OBJECT: 0xFFFFFFFF // all objects + + }; + /** + * Initialize the collision/physic world + * @ignore + */ + + api.init = function () { + // default bounds to the game world size + api.bounds = me.game.world.getBounds().clone(); // initializa the quadtree + + api.quadTree = new me.QuadTree(api.bounds, api.maxChildren, api.maxDepth); // reset the collision detection engine if a new level is loaded + + me.event.subscribe(me.event.LEVEL_LOADED, function () { + // align default bounds to the game world bounds + api.bounds.copy(me.game.world.getBounds()); // reset the quadtree + + api.quadTree.clear(api.bounds); + }); + }; + /** + * An object representing the result of an intersection. + * @property {me.Renderable} a The first object participating in the intersection + * @property {me.Renderable} b The second object participating in the intersection + * @property {Number} overlap Magnitude of the overlap on the shortest colliding axis + * @property {me.Vector2d} overlapV The overlap vector (i.e. `overlapN.scale(overlap, overlap)`). If this vector is subtracted from the position of a, a and b will no longer be colliding + * @property {me.Vector2d} overlapN The shortest colliding axis (unit-vector) + * @property {Boolean} aInB Whether the first object is entirely inside the second + * @property {Boolean} bInA Whether the second object is entirely inside the first + * @property {Number} indexShapeA The index of the colliding shape for the object a body + * @property {Number} indexShapeB The index of the colliding shape for the object b body + * @name ResponseObject + * @memberOf me.collision + * @public + * @type {Object} + * @see me.collision.check + */ + + + api.ResponseObject = function () { + this.a = null; + this.b = null; + this.overlapN = new me.Vector2d(); + this.overlapV = new me.Vector2d(); + this.aInB = true; + this.bInA = true; + this.indexShapeA = -1; + this.indexShapeB = -1; + this.overlap = Number.MAX_VALUE; + }; + /** + * Set some values of the response back to their defaults.
+ * Call this between tests if you are going to reuse a single
+ * Response object for multiple intersection tests
+ * (recommended as it will avoid allocating extra memory)
+ * @name clear + * @memberOf me.collision.ResponseObject + * @public + * @function + */ + + + api.ResponseObject.prototype.clear = function () { + this.aInB = true; + this.bInA = true; + this.overlap = Number.MAX_VALUE; + this.indexShapeA = -1; + this.indexShapeB = -1; + return this; + }; + /** + * a global instance of a response object used for collision detection
+ * this object will be reused amongst collision detection call if not user-defined response is specified + * @name response + * @memberOf me.collision + * @public + * @type {me.collision.ResponseObject} + */ + + + api.response = new api.ResponseObject(); + /** + * a callback used to determine if two objects should collide (based on both respective objects collision mask and type).
+ * you can redefine this function if you need any specific rules over what should collide with what. + * @name shouldCollide + * @memberOf me.collision + * @public + * @function + * @param {me.Renderable} a a reference to the object A. + * @param {me.Renderable} b a reference to the object B. + * @return {Boolean} true if they should collide, false otherwise + */ + + api.shouldCollide = function (a, b) { + return a.isKinematic !== true && b.isKinematic !== true && a.body && b.body && (a.body.collisionMask & b.body.collisionType) !== 0 && (a.body.collisionType & b.body.collisionMask) !== 0; + }; + /** + * Checks if the specified entity collides with others entities + * @name check + * @memberOf me.collision + * @public + * @function + * @param {me.Renderable} obj entity to be tested for collision + * @param {me.collision.ResponseObject} [respObj=me.collision.response] a user defined response object that will be populated if they intersect. + * @return {Boolean} in case of collision, false otherwise + * @example + * update : function (dt) { + * // ... + * + * // handle collisions against other shapes + * me.collision.check(this); + * + * // ... + * }, + * + * // colision handler + * onCollision : function (response) { + * if (response.b.body.collisionType === me.collision.types.ENEMY_OBJECT) { + * // makes the other entity solid, by substracting the overlap vector to the current position + * this.pos.sub(response.overlapV); + * this.hurt(); + * // not solid + * return false; + * } + * // Make the object solid + * return true; + * }, + */ + + + api.check = function (objA, responseObject) { + var collision = 0; + var response = responseObject || api.response; // retreive a list of potential colliding objects + + var candidates = api.quadTree.retrieve(objA); + + for (var i = candidates.length, objB; i--, objB = candidates[i];) { + // check if both objects "should" collide + if (objB !== objA && api.shouldCollide(objA, objB) && // fast AABB check if both bounding boxes are overlaping + objA.getBounds().overlaps(objB.getBounds())) { + // go trough all defined shapes in A + var aLen = objA.body.shapes.length; + var bLen = objB.body.shapes.length; + + if (aLen === 0 || bLen === 0) { + continue; + } + + var indexA = 0; + + do { + var shapeA = objA.body.getShape(indexA); // go through all defined shapes in B + + var indexB = 0; + + do { + var shapeB = objB.body.getShape(indexB); // full SAT collision check + + if (api["test" + shapeA.shapeType + shapeB.shapeType].call(this, objA, // a reference to the object A + shapeA, objB, // a reference to the object B + shapeB, // clear response object before reusing + response.clear()) === true) { + // we touched something ! + collision++; // set the shape index + + response.indexShapeA = indexA; + response.indexShapeB = indexB; // execute the onCollision callback + + if (objA.onCollision && objA.onCollision(response, objB) !== false) { + objA.body.respondToCollision.call(objA.body, response); + } + + if (objB.onCollision && objB.onCollision(response, objA) !== false) { + objB.body.respondToCollision.call(objB.body, response); + } + } + + indexB++; + } while (indexB < bLen); + + indexA++; + } while (indexA < aLen); + } + } // we could return the amount of objects we collided with ? + + + return collision > 0; + }; + /** + * Checks for object colliding with the given line + * @name rayCast + * @memberOf me.collision + * @public + * @function + * @param {me.Line} line line to be tested for collision + * @param {Array.} [result] a user defined array that will be populated with intersecting physic objects. + * @return {Array.} an array of intersecting physic objects + * @example + * // define a line accross the viewport + * var ray = new me.Line( + * // absolute position of the line + * 0, 0, [ + * // starting point relative to the initial position + * new me.Vector2d(0, 0), + * // ending point + * new me.Vector2d(me.game.viewport.width, me.game.viewport.height) + * ]); + * + * // check for collition + * result = me.collision.rayCast(ray); + * + * if (result.length > 0) { + * // ... + * } + */ + + + api.rayCast = function (line, resultArray) { + var collision = 0; + var result = resultArray || []; // retrieve a list of potential colliding objects + + var candidates = api.quadTree.retrieve(line.getBounds()); + + for (var i = candidates.length, objB; i--, objB = candidates[i];) { + // fast AABB check if both bounding boxes are overlaping + if (objB.body && line.getBounds().overlaps(objB.getBounds())) { + // go trough all defined shapes in B (if any) + var bLen = objB.body.shapes.length; + + if (objB.body.shapes.length === 0) { + continue; + } + + var shapeA = line; // go through all defined shapes in B + + var indexB = 0; + + do { + var shapeB = objB.body.getShape(indexB); // full SAT collision check + + if (api["test" + shapeA.shapeType + shapeB.shapeType].call(this, dummyEntity, // a reference to the object A + shapeA, objB, // a reference to the object B + shapeB)) { + // we touched something ! + result[collision] = objB; + collision++; + } + + indexB++; + } while (indexB < bLen); + } + } // cap result in case it was not empty + + + result.length = collision; // return the list of colliding objects + + return result; + }; + /** + * Checks whether polygons collide. + * @ignore + * @param {me.Renderable} a a reference to the object A. + * @param {me.Polygon} polyA a reference to the object A Polygon to be tested + * @param {me.Renderable} b a reference to the object B. + * @param {me.Polygon} polyB a reference to the object B Polygon to be tested + * @param {Response=} response Response object (optional) that will be populated if they intersect. + * @return {boolean} true if they intersect, false if they don't. + */ + + + api.testPolygonPolygon = function (a, polyA, b, polyB, response) { + // specific point for + var aPoints = polyA.points; + var aNormals = polyA.normals; + var aLen = aNormals.length; + var bPoints = polyB.points; + var bNormals = polyB.normals; + var bLen = bNormals.length; // aboslute shape position + + var posA = T_VECTORS.pop().copy(a.pos).add(a.ancestor._absPos).add(polyA.pos); + var posB = T_VECTORS.pop().copy(b.pos).add(b.ancestor._absPos).add(polyB.pos); + var i; // If any of the edge normals of A is a separating axis, no intersection. + + for (i = 0; i < aLen; i++) { + if (isSeparatingAxis(posA, posB, aPoints, bPoints, aNormals[i], response)) { + T_VECTORS.push(posA); + T_VECTORS.push(posB); + return false; + } + } // If any of the edge normals of B is a separating axis, no intersection. + + + for (i = 0; i < bLen; i++) { + if (isSeparatingAxis(posA, posB, aPoints, bPoints, bNormals[i], response)) { + T_VECTORS.push(posA); + T_VECTORS.push(posB); + return false; + } + } // Since none of the edge normals of A or B are a separating axis, there is an intersection + // and we've already calculated the smallest overlap (in isSeparatingAxis). Calculate the + // final overlap vector. + + + if (response) { + response.a = a; + response.b = b; + response.overlapV.copy(response.overlapN).scale(response.overlap); + } + + T_VECTORS.push(posA); + T_VECTORS.push(posB); + return true; + }; + /** + * Check if two Ellipse collide. + * @ignore + * @param {me.Renderable} a a reference to the object A. + * @param {me.Ellipse} ellipseA a reference to the object A Ellipse to be tested + * @param {me.Renderable} b a reference to the object B. + * @param {me.Ellipse} ellipseB a reference to the object B Ellipse to be tested + * @param {Response=} response Response object (optional) that will be populated if + * the circles intersect. + * @return {boolean} true if the circles intersect, false if they don't. + */ + + + api.testEllipseEllipse = function (a, ellipseA, b, ellipseB, response) { + // Check if the distance between the centers of the two + // circles is greater than their combined radius. + var differenceV = T_VECTORS.pop().copy(b.pos).add(b.ancestor._absPos).add(ellipseB.pos).sub(a.pos).add(a.ancestor._absPos).sub(ellipseA.pos); + var radiusA = ellipseA.radius; + var radiusB = ellipseB.radius; + var totalRadius = radiusA + radiusB; + var totalRadiusSq = totalRadius * totalRadius; + var distanceSq = differenceV.length2(); // If the distance is bigger than the combined radius, they don't intersect. + + if (distanceSq > totalRadiusSq) { + T_VECTORS.push(differenceV); + return false; + } // They intersect. If we're calculating a response, calculate the overlap. + + + if (response) { + var dist = Math.sqrt(distanceSq); + response.a = a; + response.b = b; + response.overlap = totalRadius - dist; + response.overlapN.copy(differenceV.normalize()); + response.overlapV.copy(differenceV).scale(response.overlap); + response.aInB = radiusA <= radiusB && dist <= radiusB - radiusA; + response.bInA = radiusB <= radiusA && dist <= radiusA - radiusB; + } + + T_VECTORS.push(differenceV); + return true; + }; + /** + * Check if a polygon and an ellipse collide. + * @ignore + * @param {me.Renderable} a a reference to the object A. + * @param {me.Polygon} polyA a reference to the object A Polygon to be tested + * @param {me.Renderable} b a reference to the object B. + * @param {me.Ellipse} ellipseB a reference to the object B Ellipse to be tested + * @param {Response=} response Response object (optional) that will be populated if they intersect. + * @return {boolean} true if they intersect, false if they don't. + */ + + + api.testPolygonEllipse = function (a, polyA, b, ellipseB, response) { + // Get the position of the circle relative to the polygon. + var circlePos = T_VECTORS.pop().copy(b.pos).add(b.ancestor._absPos).add(ellipseB.pos).sub(a.pos).add(a.ancestor._absPos).sub(polyA.pos); + var radius = ellipseB.radius; + var radius2 = radius * radius; + var points = polyA.points; + var edges = polyA.edges; + var len = edges.length; + var edge = T_VECTORS.pop(); + var normal = T_VECTORS.pop(); + var point = T_VECTORS.pop(); + var dist = 0; // For each edge in the polygon: + + for (var i = 0; i < len; i++) { + var next = i === len - 1 ? 0 : i + 1; + var prev = i === 0 ? len - 1 : i - 1; + var overlap = 0; + var overlapN = null; // Get the edge. + + edge.copy(edges[i]); // Calculate the center of the circle relative to the starting point of the edge. + + point.copy(circlePos).sub(points[i]); // If the distance between the center of the circle and the point + // is bigger than the radius, the polygon is definitely not fully in + // the circle. + + if (response && point.length2() > radius2) { + response.aInB = false; + } // Calculate which Vornoi region the center of the circle is in. + + + var region = vornoiRegion(edge, point); + var inRegion = true; // If it's the left region: + + if (region === LEFT_VORNOI_REGION) { + var point2 = null; + + if (len > 1) { + // We need to make sure we're in the RIGHT_VORNOI_REGION of the previous edge. + edge.copy(edges[prev]); // Calculate the center of the circle relative the starting point of the previous edge + + point2 = T_VECTORS.pop().copy(circlePos).sub(points[prev]); + region = vornoiRegion(edge, point2); + + if (region !== RIGHT_VORNOI_REGION) { + inRegion = false; + } + } + + if (inRegion) { + // It's in the region we want. Check if the circle intersects the point. + dist = point.length(); + + if (dist > radius) { + // No intersection + T_VECTORS.push(circlePos); + T_VECTORS.push(edge); + T_VECTORS.push(normal); + T_VECTORS.push(point); + + if (point2) { + T_VECTORS.push(point2); + } + + return false; + } else if (response) { + // It intersects, calculate the overlap. + response.bInA = false; + overlapN = point.normalize(); + overlap = radius - dist; + } + } + + if (point2) { + T_VECTORS.push(point2); + } // If it's the right region: + + } else if (region === RIGHT_VORNOI_REGION) { + if (len > 1) { + // We need to make sure we're in the left region on the next edge + edge.copy(edges[next]); // Calculate the center of the circle relative to the starting point of the next edge. + + point.copy(circlePos).sub(points[next]); + region = vornoiRegion(edge, point); + + if (region !== LEFT_VORNOI_REGION) { + inRegion = false; + } + } + + if (inRegion) { + // It's in the region we want. Check if the circle intersects the point. + dist = point.length(); + + if (dist > radius) { + // No intersection + T_VECTORS.push(circlePos); + T_VECTORS.push(edge); + T_VECTORS.push(normal); + T_VECTORS.push(point); + return false; + } else if (response) { + // It intersects, calculate the overlap. + response.bInA = false; + overlapN = point.normalize(); + overlap = radius - dist; + } + } // Otherwise, it's the middle region: + + } else { + // Need to check if the circle is intersecting the edge, + // Get the normal. + normal.copy(polyA.normals[i]); // Find the perpendicular distance between the center of the + // circle and the edge. + + dist = point.dotProduct(normal); + var distAbs = Math.abs(dist); // If the circle is on the outside of the edge, there is no intersection. + + if ((len === 1 || dist > 0) && distAbs > radius) { + // No intersection + T_VECTORS.push(circlePos); + T_VECTORS.push(edge); + T_VECTORS.push(normal); + T_VECTORS.push(point); + return false; + } else if (response) { + // It intersects, calculate the overlap. + overlapN = normal; + overlap = radius - dist; // If the center of the circle is on the outside of the edge, or part of the + // circle is on the outside, the circle is not fully inside the polygon. + + if (dist >= 0 || overlap < 2 * radius) { + response.bInA = false; + } + } + } // If this is the smallest overlap we've seen, keep it. + // (overlapN may be null if the circle was in the wrong Vornoi region). + + + if (overlapN && response && Math.abs(overlap) < Math.abs(response.overlap)) { + response.overlap = overlap; + response.overlapN.copy(overlapN); + } + } // Calculate the final overlap vector - based on the smallest overlap. + + + if (response) { + response.a = a; + response.b = b; + response.overlapV.copy(response.overlapN).scale(response.overlap); + } + + T_VECTORS.push(circlePos); + T_VECTORS.push(edge); + T_VECTORS.push(normal); + T_VECTORS.push(point); + return true; + }; + /** + * Check if an ellipse and a polygon collide.
+ * **NOTE:** This is slightly less efficient than testPolygonEllipse as it just + * runs testPolygonEllipse and reverses the response at the end. + * @ignore + * @param {me.Renderable} a a reference to the object A. + * @param {me.Ellipse} ellipseA a reference to the object A Ellipse to be tested + * @param {me.Renderable} a a reference to the object B. + * @param {me.Polygon} polyB a reference to the object B Polygon to be tested + * @param {Response=} response Response object (optional) that will be populated if + * they intersect. + * @return {boolean} true if they intersect, false if they don't. + */ + + + api.testEllipsePolygon = function (a, ellipseA, b, polyB, response) { + // Test the polygon against the circle. + var result = api.testPolygonEllipse(b, polyB, a, ellipseA, response); + + if (result && response) { + // Swap A and B in the response. + var resa = response.a; + var aInB = response.aInB; + response.overlapN.negateSelf(); + response.overlapV.negateSelf(); + response.a = response.b; + response.b = resa; + response.aInB = response.bInA; + response.bInA = aInB; + } + + return result; + }; // return our object + + + return api; + }(); + })(); + + (function () { + /** + * A base class for renderable objects. + * @class + * @extends me.Rect + * @memberOf me + * @constructor + * @param {Number} x position of the renderable object (accessible through inherited pos.x property) + * @param {Number} y position of the renderable object (accessible through inherited pos.y property) + * @param {Number} width object width + * @param {Number} height object height + */ + me.Renderable = me.Rect.extend({ + /** + * @ignore + */ + init: function init(x, y, width, height) { + /** + * to identify the object as a renderable object + * @ignore + */ + this.isRenderable = true; + /** + * If true then physic collision and input events will not impact this renderable + * @public + * @type Boolean + * @default true + * @name isKinematic + * @memberOf me.Renderable + */ + + this.isKinematic = true; + /** + * the renderable physic body + * @public + * @type {me.Body} + * @see me.Body + * @see me.collision.check + * @name body + * @memberOf me.Renderable# + * @example + * // define a new Player Class + * game.PlayerEntity = me.Sprite.extend({ + * // constructor + * init:function (x, y, settings) { + * // call the parent constructor + * this._super(me.Sprite, 'init', [x, y , settings]); + * + * // define a basic walking animation + * this.addAnimation("walk", [...]); + * // define a standing animation (using the first frame) + * this.addAnimation("stand", [...]); + * // set the standing animation as default + * this.setCurrentAnimation("stand"); + * + * // add a physic body + * this.body = new me.Body(this); + * // add a default collision shape + * this.body.addShape(new me.Rect(0, 0, this.width, this.height)); + * // configure max speed and friction + * this.body.setMaxVelocity(3, 15); + * this.body.setFriction(0.4, 0); + * + * // enable physic collision (off by default for basic me.Renderable) + * this.isKinematic = false; + * + * // set the display to follow our position on both axis + * me.game.viewport.follow(this.pos, me.game.viewport.AXIS.BOTH); + * }, + * + * ... + * + * } + */ + + this.body = undefined; + /** + * the renderable default transformation matrix + * @public + * @type me.Matrix2d + * @name currentTransform + * @memberOf me.Renderable# + */ + + if (typeof this.currentTransform === "undefined") { + this.currentTransform = me.pool.pull("me.Matrix2d"); + } + + this.currentTransform.identity(); + /** + * (G)ame (U)nique (Id)entifier"
+ * a GUID will be allocated for any renderable object added
+ * to an object container (including the `me.game.world` container) + * @public + * @type String + * @name GUID + * @memberOf me.Renderable + */ + + this.GUID = undefined; + /** + * an event handler that is called when the renderable leave or enter a camera viewport + * @public + * @type function + * @default undefined + * @name onVisibilityChange + * @memberOf me.Renderable# + * @example + * this.onVisibilityChange = function(inViewport) { + * if (inViewport === true) { + * console.log("object has entered the in a camera viewport!"); + * } + * }; + */ + + this.onVisibilityChange = undefined; + /** + * Whether the renderable object will always update, even when outside of the viewport
+ * @public + * @type Boolean + * @default false + * @name alwaysUpdate + * @memberOf me.Renderable + */ + + this.alwaysUpdate = false; + /** + * Whether to update this object when the game is paused. + * @public + * @type Boolean + * @default false + * @name updateWhenPaused + * @memberOf me.Renderable + */ + + this.updateWhenPaused = false; + /** + * make the renderable object persistent over level changes
+ * @public + * @type Boolean + * @default false + * @name isPersistent + * @memberOf me.Renderable + */ + + this.isPersistent = false; + /** + * If true, this renderable will be rendered using screen coordinates, + * as opposed to world coordinates. Use this, for example, to define UI elements. + * @public + * @type Boolean + * @default false + * @name floating + * @memberOf me.Renderable + */ + + this.floating = false; + /** + * The anchor point is used for attachment behavior, and/or when applying transformations.
+ * The coordinate system places the origin at the top left corner of the frame (0, 0) and (1, 1) means the bottom-right corner
+ * :
+ * a Renderable's anchor point defaults to (0.5,0.5), which corresponds to the center position.
+ * @public + * @type me.ObservableVector2d + * @default <0.5,0.5> + * @name anchorPoint + * @memberOf me.Renderable# + */ + + if (this.anchorPoint instanceof me.ObservableVector2d) { + this.anchorPoint.setMuted(0.5, 0.5).setCallback(this.onAnchorUpdate.bind(this)); + } else { + this.anchorPoint = me.pool.pull("me.ObservableVector2d", 0.5, 0.5, { + onUpdate: this.onAnchorUpdate.bind(this) + }); + } + /** + * When enabled, an object container will automatically apply + * any defined transformation before calling the child draw method. + * @public + * @type Boolean + * @default true + * @name autoTransform + * @memberOf me.Renderable + * @example + * // enable "automatic" transformation when the object is activated + * onActivateEvent: function () { + * // reset the transformation matrix + * this.renderable.currentTransform.identity(); + * // ensure the anchor point is the renderable center + * this.renderable.anchorPoint.set(0.5, 0.5); + * // enable auto transform + * this.renderable.autoTransform = true; + * .... + * }, + * // add a rotation effect when updating the entity + * update : function (dt) { + * .... + * this.renderable.currentTransform.rotate(0.025); + * .... + * return this._super(me.Entity, 'update', [dt]); + * }, + */ + + + this.autoTransform = true; + /** + * Define the renderable opacity
+ * Set to zero if you do not wish an object to be drawn + * @see me.Renderable#setOpacity + * @see me.Renderable#getOpacity + * @public + * @type Number + * @default 1.0 + * @name me.Renderable#alpha + */ + + this.alpha = 1.0; + /** + * a reference to the parent object that contains this renderable + * @public + * @type me.Container|me.Entity + * @default undefined + * @name me.Renderable#ancestor + */ + + this.ancestor = undefined; + /** + * The bounding rectangle for this renderable + * @ignore + * @type {me.Rect} + * @name _bounds + * @memberOf me.Renderable + */ + + if (this._bounds instanceof me.Rect) { + this._bounds.setShape(x, y, width, height); + } else { + this._bounds = me.pool.pull("me.Rect", x, y, width, 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. + * @public + * @type {me.Rect|me.Polygon|me.Line|me.Ellipse} + * @name mask + * @default undefined + * @memberOf me.Renderable# + * @example + * // apply a mask in the shape of a Star + * myNPCSprite.mask = new me.Polygon(myNPCSprite.width / 2, 0, [ + * // draw a star + * {x: 0, y: 0}, + * {x: 14, y: 30}, + * {x: 47, y: 35}, + * {x: 23, y: 57}, + * {x: 44, y: 90}, + * {x: 0, y: 62}, + * {x: -44, y: 90}, + * {x: -23, y: 57}, + * {x: -47, y: 35}, + * {x: -14, y: 30} + * ]); + */ + + + this.mask = undefined; + /** + * apply a tint to this renderable (WebGL Only) + * @public + * @type {me.Color} + * @name tint + * @default undefined + * @memberOf me.Renderable# + * @example + * // add a red tint to this renderable + * this.renderable.tint = new me.Color(255, 128, 128); + * // disable the tint + * this.renderable.tint.setColor(255, 255, 255); + */ + + this.tint = undefined; + /** + * The name of the renderable + * @public + * @type {String} + * @name name + * @default "" + * @memberOf me.Renderable + */ + + this.name = ""; + /** + * Absolute position in the game world + * @ignore + * @type {me.Vector2d} + * @name _absPos + * @memberOf me.Renderable# + */ + + if (this._absPos instanceof me.Vector2d) { + this._absPos.set(x, y); + } else { + this._absPos = me.pool.pull("me.Vector2d", x, y); + } + /** + * Position of the Renderable relative to its parent container + * @public + * @type {me.ObservableVector3d} + * @name pos + * @memberOf me.Renderable# + */ + + + if (this.pos instanceof me.ObservableVector3d) { + this.pos.setMuted(x, y, 0).setCallback(this.updateBoundsPos.bind(this)); + } else { + this.pos = me.pool.pull("me.ObservableVector3d", x, y, 0, { + onUpdate: this.updateBoundsPos.bind(this) + }); + } + + this._width = width; + this._height = height; // keep track of when we flip + + this._flip = { + x: false, + y: false + }; // viewport flag + + this._inViewport = false; + this.shapeType = "Rectangle"; // ensure it's fully opaque by default + + this.setOpacity(1.0); + }, + + /** @ignore */ + onResetEvent: function onResetEvent() { + this.init.apply(this, arguments); + }, + + /** + * returns the bounding box for this renderable + * @name getBounds + * @memberOf me.Renderable.prototype + * @function + * @return {me.Rect} bounding box Rectangle object + */ + getBounds: function getBounds() { + return this._bounds; + }, + + /** + * get the renderable alpha channel value
+ * @name getOpacity + * @memberOf me.Renderable.prototype + * @function + * @return {Number} current opacity value between 0 and 1 + */ + getOpacity: function getOpacity() { + return this.alpha; + }, + + /** + * set the renderable alpha channel value
+ * @name setOpacity + * @memberOf me.Renderable.prototype + * @function + * @param {Number} alpha opacity value between 0.0 and 1.0 + */ + setOpacity: function setOpacity(alpha) { + if (typeof alpha === "number") { + this.alpha = me.Math.clamp(alpha, 0.0, 1.0); // Set to 1 if alpha is NaN + + if (isNaN(this.alpha)) { + this.alpha = 1.0; + } + } + }, + + /** + * flip the renderable on the horizontal axis (around the center of the renderable) + * @see me.Matrix2d.scaleX + * @name flipX + * @memberOf me.Renderable.prototype + * @function + * @param {Boolean} [flip=false] `true` to flip this renderable. + * @return {me.Renderable} Reference to this object for method chaining + */ + flipX: function flipX(flip) { + this._flip.x = !!flip; + return this; + }, + + /** + * flip the renderable on the vertical axis (around the center of the renderable) + * @see me.Matrix2d.scaleY + * @name flipY + * @memberOf me.Renderable.prototype + * @function + * @param {Boolean} [flip=false] `true` to flip this renderable. + * @return {me.Renderable} Reference to this object for method chaining + */ + flipY: function flipY(flip) { + this._flip.y = !!flip; + return this; + }, + + /** + * multiply the renderable currentTransform with the given matrix + * @name transform + * @memberOf me.Renderable.prototype + * @see me.Renderable#currentTransform + * @function + * @param {me.Matrix2d} matrix the transformation matrix + * @return {me.Renderable} Reference to this object for method chaining + */ + transform: function transform(m) { + var bounds = this.getBounds(); + this.currentTransform.multiply(m); + bounds.setPoints(bounds.transform(m).points); + bounds.pos.setV(this.pos); + return this; + }, + + /** + * scale the renderable around his anchor point. Scaling actually applies changes + * to the currentTransform member wich is used by the renderer to scale the object + * when rendering. It does not scale the object itself. For example if the renderable + * is an image, the image.width and image.height properties are unaltered but the currentTransform + * member will be changed. + * @name scale + * @memberOf me.Renderable.prototype + * @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.Renderable} Reference to this object for method chaining + */ + scale: function scale(x, y) { + var _x = x, + _y = typeof y === "undefined" ? _x : y; // set the scaleFlag + + + this.currentTransform.scale(_x, _y); // resize the bounding box + + this.getBounds().resize(this.width * _x, this.height * _y); + return this; + }, + + /** + * scale the renderable around his anchor point + * @name scaleV + * @memberOf me.Renderable.prototype + * @function + * @param {me.Vector2d} vector scaling vector + * @return {me.Renderable} Reference to this object for method chaining + */ + scaleV: function scaleV(v) { + this.scale(v.x, v.y); + return this; + }, + + /** + * update function.
+ * automatically called by the game manager {@link me.game} + * @name update + * @memberOf me.Renderable.prototype + * @function + * @protected + * @param {Number} dt time since the last update in milliseconds. + * @return false + **/ + update: function update() + /* dt */ + { + return false; + }, + + /** + * update the renderable's bounding rect (private) + * @ignore + * @name updateBoundsPos + * @memberOf me.Renderable.prototype + * @function + */ + updateBoundsPos: function updateBoundsPos(newX, newY) { + var bounds = this.getBounds(); + bounds.pos.set(newX, newY, bounds.pos.z); // XXX: This is called from the constructor, before it gets an ancestor + + if (this.ancestor instanceof me.Container && !this.floating) { + bounds.pos.add(this.ancestor._absPos); + } + + return bounds; + }, + + /** + * called when the anchor point value is changed + * @private + * @name onAnchorUpdate + * @memberOf me.Renderable.prototype + * @function + */ + onAnchorUpdate: function onAnchorUpdate() { + }, + + /** + * update the bounds + * @private + * @deprecated + * @name updateBounds + * @memberOf me.Renderable.prototype + * @function + */ + updateBounds: function updateBounds() { + console.warn("Deprecated: me.Renderable.updateBounds"); + return this._super(me.Rect, "updateBounds"); + }, + + /** + * prepare the rendering context before drawing + * (apply defined transforms, anchor point).
+ * automatically called by the game manager {@link me.game} + * @name preDraw + * @memberOf me.Renderable.prototype + * @function + * @protected + * @param {me.CanvasRenderer|me.WebGLRenderer} renderer a renderer object + **/ + preDraw: function preDraw(renderer) { + var bounds = this.getBounds(); + var ax = bounds.width * this.anchorPoint.x, + ay = bounds.height * this.anchorPoint.y; // save context + + renderer.save(); // apply the defined alpha value + + renderer.setGlobalAlpha(renderer.globalAlpha() * this.getOpacity()); // apply flip + + if (this._flip.x || this._flip.y) { + var dx = this._flip.x ? this.centerX - ax : 0, + dy = this._flip.y ? this.centerY - ay : 0; + renderer.translate(dx, dy); + renderer.scale(this._flip.x ? -1 : 1, this._flip.y ? -1 : 1); + renderer.translate(-dx, -dy); + } + + if (this.autoTransform === true && !this.currentTransform.isIdentity()) { + this.currentTransform.translate(-ax, -ay); // apply the renderable transformation matrix + + renderer.transform(this.currentTransform); + this.currentTransform.translate(ax, ay); + } else { + // translate to the defined anchor point + renderer.translate(-ax, -ay); + } + + if (typeof this.mask !== "undefined") { + renderer.setMask(this.mask); + } + + if (typeof this.tint !== "undefined") { + renderer.setTint(this.tint); + } + }, + + /** + * object draw.
+ * automatically called by the game manager {@link me.game} + * @name draw + * @memberOf me.Renderable.prototype + * @function + * @protected + * @param {me.CanvasRenderer|me.WebGLRenderer} renderer a renderer object + **/ + draw: function draw() + /*renderer*/ + {// empty one ! + }, + + /** + * restore the rendering context after drawing.
+ * automatically called by the game manager {@link me.game} + * @name postDraw + * @memberOf me.Renderable.prototype + * @function + * @protected + * @param {me.CanvasRenderer|me.WebGLRenderer} renderer a renderer object + **/ + postDraw: function postDraw(renderer) { + if (typeof this.mask !== "undefined") { + renderer.clearMask(); + } + + if (typeof this.tint !== "undefined") { + renderer.clearTint(); + } // restore the context + + + renderer.restore(); + }, + + /** + * Destroy function
+ * @ignore + */ + destroy: function destroy() { + // allow recycling object properties + me.pool.push(this.currentTransform); + this.currentTransform = undefined; + me.pool.push(this.anchorPoint); + this.anchorPoint = undefined; + me.pool.push(this.pos); + this.pos = undefined; + me.pool.push(this._absPos); + this._absPos = undefined; + me.pool.push(this._bounds); + this._bounds = undefined; + this.onVisibilityChange = undefined; + + if (typeof this.mask !== "undefined") { + me.pool.push(this.mask); + this.mask = undefined; + } + + if (typeof this.tint !== "undefined") { + me.pool.push(this.tint); + this.tint = undefined; + } + + this.ancestor = undefined; // destroy the physic body if defined + + if (typeof this.body !== "undefined") { + this.body.destroy.apply(this.body, arguments); + this.body = undefined; + } + + this.onDestroyEvent.apply(this, arguments); + }, + + /** + * OnDestroy Notification function
+ * Called by engine before deleting the object + * @name onDestroyEvent + * @memberOf me.Renderable + * @function + */ + onDestroyEvent: function onDestroyEvent() {// to be extended ! + } + }); + /** + * Whether the renderable object is visible and within the viewport + * @public + * @readonly + * @type Boolean + * @default false + * @name inViewport + * @memberOf me.Renderable + */ + + Object.defineProperty(me.Renderable.prototype, "inViewport", { + /** + * @ignore + */ + get: function get() { + return this._inViewport; + }, + + /** + * @ignore + */ + set: function set(value) { + if (this._inViewport !== value) { + this._inViewport = value; + + if (typeof this.onVisibilityChange === "function") { + this.onVisibilityChange.call(this, value); + } + } + }, + configurable: true + }); + /** + * width of the Renderable bounding box + * @public + * @type {Number} + * @name width + * @memberOf me.Renderable + */ + + Object.defineProperty(me.Renderable.prototype, "width", { + /** + * @ignore + */ + get: function get() { + return this._width; + }, + + /** + * @ignore + */ + set: function set(value) { + if (this._width !== value) { + this.getBounds().width = value; + this._width = value; + } + }, + configurable: true + }); + /** + * height of the Renderable bounding box + * @public + * @type {Number} + * @name height + * @memberOf me.Renderable + */ + + Object.defineProperty(me.Renderable.prototype, "height", { + /** + * @ignore + */ + get: function get() { + return this._height; + }, + + /** + * @ignore + */ + set: function set(value) { + if (this._height !== value) { + this.getBounds().height = value; + this._height = value; + } + }, + configurable: true + }); + })(); + + (function () { + /** + * a generic Color Layer Object. Fills the entire Canvas with the color not just the container the object belongs to. + * @class + * @extends me.Renderable + * @memberOf me + * @constructor + * @param {String} name Layer name + * @param {me.Color|String} color CSS color + * @param {Number} z z-index position + */ + me.ColorLayer = me.Renderable.extend({ + /** + * @ignore + */ + init: function init(name, color, z) { + // parent constructor + this._super(me.Renderable, "init", [0, 0, Infinity, Infinity]); // apply given parameters + + + this.name = name; + this.pos.z = z; + this.floating = true; + /** + * the layer color component + * @public + * @type me.Color + * @name color + * @memberOf me.ColorLayer# + */ + // parse the given color + + if (color instanceof me.Color) { + this.color = color; + } else { + // string (#RGB, #ARGB, #RRGGBB, #AARRGGBB) + this.color = me.pool.pull("me.Color").parseCSS(color); + } + + this.anchorPoint.set(0, 0); + }, + + /** + * draw the color layer + * @ignore + */ + draw: function draw(renderer, rect) { + var color = renderer.getColor(); + var vpos = me.game.viewport.pos; + renderer.setColor(this.color); + renderer.fillRect(rect.left - vpos.x, rect.top - vpos.y, rect.width, rect.height); + renderer.setColor(color); + }, + + /** + * Destroy function + * @ignore + */ + destroy: function destroy() { + me.pool.push(this.color); + this.color = undefined; + + this._super(me.Renderable, "destroy"); + } + }); + })(); + + (function () { + /** + * a generic Image Layer Object + * @class + * @extends me.Renderable + * @memberOf me + * @constructor + * @param {Number} x x coordinate + * @param {Number} y y coordinate + * @param {Object} settings ImageLayer properties + * @param {HTMLImageElement|HTMLCanvasElement|String} settings.image Image reference. See {@link me.loader.getImage} + * @param {String} [settings.name="me.ImageLayer"] layer name + * @param {Number} [settings.z=0] z-index position + * @param {Number|me.Vector2d} [settings.ratio=1.0] Scrolling ratio to be applied. See {@link me.ImageLayer#ratio} + * @param {String} [settings.repeat='repeat'] define if and how an Image Layer should be repeated (accepted values are 'repeat', + 'repeat-x', 'repeat-y', 'no-repeat'). See {@link me.ImageLayer#repeat} + * @param {Number|me.Vector2d} [settings.anchorPoint=0.0] Image origin. See {@link me.ImageLayer#anchorPoint} + * @example + * // create a repetitive background pattern on the X axis using the citycloud image asset + * me.game.world.addChild(new me.ImageLayer(0, 0, { + * image:"citycloud", + * repeat :"repeat-x" + * }), 1); + */ + me.ImageLayer = me.Renderable.extend({ + /** + * @ignore + */ + init: function init(x, y, settings) { + // call the constructor + this._super(me.Renderable, "init", [x, y, Infinity, Infinity]); // get the corresponding image + + + this.image = _typeof(settings.image) === "object" ? settings.image : me.loader.getImage(settings.image); // throw an error if image is null/undefined + + if (!this.image) { + throw new Error((typeof settings.image === "string" ? "'" + settings.image + "'" : "Image") + " file for Image Layer '" + this.name + "' not found!"); + } + + this.imagewidth = this.image.width; + this.imageheight = this.image.height; // set the sprite name if specified + + if (typeof settings.name === "string") { + this.name = settings.name; + } // render in screen coordinates + + + this.floating = true; // displaying order + + this.pos.z = settings.z || 0; + this.offset = me.pool.pull("me.Vector2d", x, y); + /** + * Define the image scrolling ratio
+ * Scrolling speed is defined by multiplying the viewport delta position (e.g. followed entity) by the specified ratio. + * Setting this vector to <0.0,0.0> will disable automatic scrolling.
+ * To specify a value through Tiled, use one of the following format :
+ * - a number, to change the value for both axis
+ * - a json expression like `json:{"x":0.5,"y":0.5}` if you wish to specify a different value for both x and y + * @public + * @type me.Vector2d + * @default <1.0,1.0> + * @name me.ImageLayer#ratio + */ + + this.ratio = me.pool.pull("me.Vector2d", 1.0, 1.0); + + if (typeof settings.ratio !== "undefined") { + // little hack for backward compatiblity + if (typeof settings.ratio === "number") { + this.ratio.set(settings.ratio, settings.ratio); + } else + /* vector */ + { + this.ratio.setV(settings.ratio); + } + } + + if (typeof settings.anchorPoint === "undefined") { + /** + * Define how the image is anchored to the viewport bounds
+ * By default, its upper-left corner is anchored to the viewport bounds upper left corner.
+ * The anchorPoint is a unit vector where each component falls in range [0.0,1.0].
+ * Some common examples:
+ * * <0.0,0.0> : (Default) Anchor image to the upper-left corner of viewport bounds + * * <0.5,0.5> : Center the image within viewport bounds + * * <1.0,1.0> : Anchor image to the lower-right corner of viewport bounds + * To specify a value through Tiled, use one of the following format :
+ * - a number, to change the value for both axis
+ * - a json expression like `json:{"x":0.5,"y":0.5}` if you wish to specify a different value for both x and y + * @public + * @type me.Vector2d + * @default <0.0,0.0> + * @name me.ImageLayer#anchorPoint + */ + this.anchorPoint.set(0, 0); + } else { + if (typeof settings.anchorPoint === "number") { + this.anchorPoint.set(settings.anchorPoint, settings.anchorPoint); + } else + /* vector */ + { + this.anchorPoint.setV(settings.anchorPoint); + } + } + /** + * Define if and how an Image Layer should be repeated.
+ * By default, an Image Layer is repeated both vertically and horizontally.
+ * Acceptable values :
+ * * 'repeat' - The background image will be repeated both vertically and horizontally
+ * * 'repeat-x' - The background image will be repeated only horizontally.
+ * * 'repeat-y' - The background image will be repeated only vertically.
+ * * 'no-repeat' - The background-image will not be repeated.
+ * @public + * @type String + * @default 'repeat' + * @name me.ImageLayer#repeat + */ + + + Object.defineProperty(this, "repeat", { + /** + * @ignore + */ + get: function get() { + return this._repeat; + }, + + /** + * @ignore + */ + set: function set(val) { + this._repeat = val; + + switch (this._repeat) { + case "no-repeat": + this.repeatX = false; + this.repeatY = false; + break; + + case "repeat-x": + this.repeatX = true; + this.repeatY = false; + break; + + case "repeat-y": + this.repeatX = false; + this.repeatY = true; + break; + + default: + // "repeat" + this.repeatX = true; + this.repeatY = true; + break; + } + + this.resize(me.game.viewport.width, me.game.viewport.height); + this.createPattern(); + }, + configurable: true + }); + this.repeat = settings.repeat || "repeat"; // on context lost, all previous textures are destroyed + + me.event.subscribe(me.event.WEBGL_ONCONTEXT_RESTORED, this.createPattern.bind(this)); + }, + // called when the layer is added to the game world or a container + onActivateEvent: function onActivateEvent() { + var _updateLayerFn = this.updateLayer.bind(this); // register to the viewport change notification + + + this.vpChangeHdlr = me.event.subscribe(me.event.VIEWPORT_ONCHANGE, _updateLayerFn); + this.vpResizeHdlr = me.event.subscribe(me.event.VIEWPORT_ONRESIZE, this.resize.bind(this)); + this.vpLoadedHdlr = me.event.subscribe(me.event.LEVEL_LOADED, function () { + // force a first refresh when the level is loaded + _updateLayerFn(me.game.viewport.pos); + }); // in case the level is not added to the root container, + // the onActivateEvent call happens after the LEVEL_LOADED event + // so we need to force a first update + + if (this.ancestor.root !== true) { + this.updateLayer(me.game.viewport.pos); + } + }, + + /** + * resize the Image Layer to match the given size + * @name resize + * @memberOf me.ImageLayer.prototype + * @function + * @param {Number} w new width + * @param {Number} h new height + */ + resize: function resize(w, h) { + this._super(me.Renderable, "resize", [this.repeatX ? Infinity : w, this.repeatY ? Infinity : h]); + }, + + /** + * createPattern function + * @ignore + * @function + */ + createPattern: function createPattern() { + this._pattern = me.video.renderer.createPattern(this.image, this._repeat); + }, + + /** + * updateLayer function + * @ignore + * @function + */ + updateLayer: function updateLayer(vpos) { + var rx = this.ratio.x, + ry = this.ratio.y; + + if (rx === ry === 0) { + // static image + return; + } + + var viewport = me.game.viewport, + width = this.imagewidth, + height = this.imageheight, + bw = viewport.bounds.width, + bh = viewport.bounds.height, + ax = this.anchorPoint.x, + ay = this.anchorPoint.y, + + /* + * Automatic positioning + * + * See https://github.com/melonjs/melonJS/issues/741#issuecomment-138431532 + * for a thorough description of how this works. + */ + x = ax * (rx - 1) * (bw - viewport.width) + this.offset.x - rx * vpos.x, + y = ay * (ry - 1) * (bh - viewport.height) + this.offset.y - ry * vpos.y; // Repeat horizontally; start drawing from left boundary + + if (this.repeatX) { + this.pos.x = x % width; + } else { + this.pos.x = x; + } // Repeat vertically; start drawing from top boundary + + + if (this.repeatY) { + this.pos.y = y % height; + } else { + this.pos.y = y; + } + }, + + /* + * override the default predraw function + * as repeat and anchor are managed directly in the draw method + * @ignore + */ + preDraw: function preDraw(renderer) { + // save the context + renderer.save(); // apply the defined alpha value + + renderer.setGlobalAlpha(renderer.globalAlpha() * this.getOpacity()); + }, + + /** + * draw the image layer + * @ignore + */ + draw: function draw(renderer) { + var viewport = me.game.viewport, + width = this.imagewidth, + height = this.imageheight, + bw = viewport.bounds.width, + bh = viewport.bounds.height, + ax = this.anchorPoint.x, + ay = this.anchorPoint.y, + x = this.pos.x, + y = this.pos.y; + + if (this.ratio.x === this.ratio.y === 0) { + x = x + ax * (bw - width); + y = y + ay * (bh - height); + } + + renderer.translate(x, y); + renderer.drawPattern(this._pattern, 0, 0, viewport.width * 2, viewport.height * 2); + }, + // called when the layer is removed from the game world or a container + onDeactivateEvent: function onDeactivateEvent() { + // cancel all event subscriptions + me.event.unsubscribe(this.vpChangeHdlr); + me.event.unsubscribe(this.vpResizeHdlr); + me.event.unsubscribe(this.vpLoadedHdlr); + }, + + /** + * Destroy function
+ * @ignore + */ + destroy: function destroy() { + me.pool.push(this.offset); + this.offset = undefined; + me.pool.push(this.ratio); + this.ratio = undefined; + + this._super(me.Renderable, "destroy"); + } + }); + })(); + + (function () { + /** + * An object to display a fixed or animated sprite on screen. + * @class + * @extends me.Renderable + * @memberOf me + * @constructor + * @param {Number} x the x coordinates of the sprite object + * @param {Number} y the y coordinates of the sprite object + * @param {Object} settings Configuration parameters for the Sprite object + * @param {me.video.renderer.Texture|HTMLImageElement|HTMLCanvasElement|String} settings.image reference to a texture, spritesheet image or to a texture atlas + * @param {String} [settings.name=""] name of this object + * @param {String} [settings.region] region name of a specific region to use when using a texture atlas, see {@link me.Renderer.Texture} + * @param {Number} [settings.framewidth] Width of a single frame within the spritesheet + * @param {Number} [settings.frameheight] Height of a single frame within the spritesheet + * @param {Number} [settings.flipX] flip the sprite on the horizontal axis + * @param {Number} [settings.flipY] flip the sprite on the vertical axis + * @param {me.Vector2d} [settings.anchorPoint={x:0.5, y:0.5}] Anchor point to draw the frame at (defaults to the center of the frame). + * @example + * // create a single sprite from a standalone image, with anchor in the center + * var sprite = new me.Sprite(0, 0, { + * image : "PlayerTexture", + * framewidth : 64, + * frameheight : 64, + * anchorPoint : new me.Vector2d(0.5, 0.5) + * }); + * + * // create a single sprite from a packed texture + * game.texture = new me.video.renderer.Texture( + * me.loader.getJSON("texture"), + * me.loader.getImage("texture") + * ); + * var sprite = new me.Sprite(0, 0, { + * image : game.texture, + * region : "npc2.png", + * }); + */ + me.Sprite = me.Renderable.extend({ + /** + * @ignore + */ + init: function init(x, y, settings) { + /** + * pause and resume animation + * @public + * @type Boolean + * @default false + * @name me.Sprite#animationpause + */ + this.animationpause = false; + /** + * animation cycling speed (delay between frame in ms) + * @public + * @type Number + * @default 100 + * @name me.Sprite#animationspeed + */ + + this.animationspeed = 100; + /** + * global offset for the position to draw from on the source image. + * @public + * @type me.Vector2d + * @default <0.0,0.0> + * @name offset + * @memberOf me.Sprite# + */ + + this.offset = me.pool.pull("me.Vector2d", 0, 0); + /** + * The source texture object this sprite object is using + * @public + * @type me.video.renderer.Texture + * @name source + * @memberOf me.Sprite# + */ + + this.source = null; // hold all defined animation + + this.anim = {}; // a flag to reset animation + + this.resetAnim = undefined; // current frame information + // (reusing current, any better/cleaner place?) + + this.current = { + // the current animation name + name: "default", + // length of the current animation name + length: 0, + //current frame texture offset + offset: new me.Vector2d(), + // current frame size + width: 0, + height: 0, + // Source rotation angle for pre-rotating the source image + angle: 0, + // current frame index + idx: 0 + }; // animation frame delta + + this.dt = 0; // flicker settings + + this._flicker = { + isFlickering: false, + duration: 0, + callback: null, + state: false + }; // call the super constructor + + this._super(me.Renderable, "init", [x, y, 0, 0]); // set the proper image/texture to use + + + if (settings.image instanceof me.Renderer.prototype.Texture) { + this.source = settings.image; + this.image = this.source.getTexture(); + this.textureAtlas = settings.image; // check for defined region + + if (typeof settings.region !== "undefined") { + // use a texture atlas + var region = this.source.getRegion(settings.region); + + if (region) { + // set the sprite region within the texture + this.setRegion(region); // update the default "current" frame size + + this.current.width = settings.framewidth || region.width; + this.current.height = settings.frameheight || region.height; + } else { + // throw an error + throw new Error("Texture - region for " + settings.region + " not found"); + } + } + } else { + // HTMLImageElement/Canvas or String + this.image = _typeof(settings.image) === "object" ? settings.image : me.loader.getImage(settings.image); // update the default "current" frame size + + this.current.width = settings.framewidth = settings.framewidth || this.image.width; + this.current.height = settings.frameheight = settings.frameheight || this.image.height; + this.source = me.video.renderer.cache.get(this.image, settings); + this.textureAtlas = this.source.getAtlas(); + } // store/reset the current atlas information if specified + + + if (typeof settings.atlas !== "undefined") { + this.textureAtlas = settings.atlas; + this.atlasIndices = settings.atlasIndices; + } else { + this.atlasIndices = null; + } // resize based on the active frame + + + this.width = this.current.width; + this.height = this.current.height; // apply flip flags if specified + + if (typeof settings.flipX !== "undefined") { + this.flipX(!!settings.flipX); + } + + if (typeof settings.flipY !== "undefined") { + this.flipY(!!settings.flipY); + } // set the default rotation angle is defined in the settings + // * WARNING: rotating sprites decreases performance with Canvas Renderer + + + if (typeof settings.rotation !== "undefined") { + this.currentTransform.rotate(settings.rotation); + } // 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; + } // for sprite, addAnimation will return !=0 + + + if (this.addAnimation("default", null) !== 0) { + // set as default + this.setCurrentAnimation("default"); + } // enable currentTransform for me.Sprite based objects + + + this.autoTransform = true; + }, + + /** + * return the flickering state of the object + * @name isFlickering + * @memberOf me.Sprite.prototype + * @function + * @return {Boolean} + */ + isFlickering: function isFlickering() { + return this._flicker.isFlickering; + }, + + /** + * make the object flicker + * @name flicker + * @memberOf me.Sprite.prototype + * @function + * @param {Number} duration expressed in milliseconds + * @param {Function} callback Function to call when flickering ends + * @return {me.Sprite} Reference to this object for method chaining + * @example + * // make the object flicker for 1 second + * // and then remove it + * this.flicker(1000, function () { + * me.game.world.removeChild(this); + * }); + */ + flicker: function flicker(duration, callback) { + this._flicker.duration = duration; + + if (this._flicker.duration <= 0) { + this._flicker.isFlickering = false; + this._flicker.callback = null; + } else if (!this._flicker.isFlickering) { + this._flicker.callback = callback; + this._flicker.isFlickering = true; + } + + return this; + }, + + /** + * add an animation
+ * For fixed-sized cell sprite sheet, the index list must follow the + * logic as per the following example :
+ * + * @name addAnimation + * @memberOf me.Sprite.prototype + * @function + * @param {String} name animation id + * @param {Number[]|String[]|Object[]} index list of sprite index or name + * defining the animation. Can also use objects to specify delay for each frame, see below + * @param {Number} [animationspeed] cycling speed for animation in ms + * @return {Number} frame amount of frame added to the animation (delay between each frame). + * @see me.Sprite#animationspeed + * @example + * // walking animation + * this.addAnimation("walk", [ 0, 1, 2, 3, 4, 5 ]); + * // standing animation + * this.addAnimation("stand", [ 11, 12 ]); + * // eating animation + * this.addAnimation("eat", [ 6, 6 ]); + * // rolling animation + * this.addAnimation("roll", [ 7, 8, 9, 10 ]); + * // slower animation + * this.addAnimation("roll", [ 7, 8, 9, 10 ], 200); + * // or get more specific with delay for each frame. Good solution instead of repeating: + * this.addAnimation("turn", [{ name: 0, delay: 200 }, { name: 1, delay: 100 }]) + * // can do this with atlas values as well: + * this.addAnimation("turn", [{ name: "turnone", delay: 200 }, { name: "turntwo", delay: 100 }]) + * // define an dying animation that stop on the last frame + * this.addAnimation("die", [{ name: 3, delay: 200 }, { name: 4, delay: 100 }, { name: 5, delay: Infinity }]) + * // set the standing animation as default + * this.setCurrentAnimation("stand"); + */ + addAnimation: function addAnimation(name, index, animationspeed) { + this.anim[name] = { + name: name, + frames: [], + idx: 0, + length: 0 + }; // # of frames + + var counter = 0; + + if (_typeof(this.textureAtlas) !== "object") { + return 0; + } + + if (index == null) { + index = []; // create a default animation with all frame + + Object.keys(this.textureAtlas).forEach(function (v, i) { + index[i] = i; + }); + } // set each frame configuration (offset, size, etc..) + + + for (var i = 0, len = index.length; i < len; i++) { + var frame = index[i]; + var frameObject; + + if (typeof frame === "number" || typeof frame === "string") { + frameObject = { + name: frame, + delay: animationspeed || this.animationspeed + }; + } else { + frameObject = frame; + } + + var frameObjectName = frameObject.name; + + if (typeof frameObjectName === "number") { + if (typeof this.textureAtlas[frameObjectName] !== "undefined") { + // TODO: adding the cache source coordinates add undefined entries in webGL mode + this.anim[name].frames[i] = Object.assign({}, this.textureAtlas[frameObjectName], frameObject); + counter++; + } + } else { + // string + if (this.atlasIndices === null) { + throw new Error("string parameters for addAnimation are not allowed for standard spritesheet based Texture"); + } else { + this.anim[name].frames[i] = Object.assign({}, this.textureAtlas[this.atlasIndices[frameObjectName]], frameObject); + counter++; + } + } + } + + this.anim[name].length = counter; + return counter; + }, + + /** + * set the current animation + * this will always change the animation & set the frame to zero + * @name setCurrentAnimation + * @memberOf me.Sprite.prototype + * @function + * @param {String} name animation id + * @param {String|Function} [onComplete] animation id to switch to when complete, or callback + * @return {me.Sprite} Reference to this object for method chaining + * @example + * // set "walk" animation + * this.setCurrentAnimation("walk"); + * + * // set "walk" animation if it is not the current animation + * if (this.isCurrentAnimation("walk")) { + * this.setCurrentAnimation("walk"); + * } + * + * // set "eat" animation, and switch to "walk" when complete + * this.setCurrentAnimation("eat", "walk"); + * + * // set "die" animation, and remove the object when finished + * this.setCurrentAnimation("die", (function () { + * me.game.world.removeChild(this); + * return false; // do not reset to first frame + * }).bind(this)); + * + * // set "attack" animation, and pause for a short duration + * this.setCurrentAnimation("die", (function () { + * this.animationpause = true; + * + * // back to "standing" animation after 1 second + * setTimeout(function () { + * this.setCurrentAnimation("standing"); + * }, 1000); + * + * return false; // do not reset to first frame + * }).bind(this)); + **/ + setCurrentAnimation: function setCurrentAnimation(name, resetAnim, _preserve_dt) { + if (this.anim[name]) { + this.current.name = name; + this.current.length = this.anim[this.current.name].length; + + if (typeof resetAnim === "string") { + this.resetAnim = this.setCurrentAnimation.bind(this, resetAnim, null, true); + } else if (typeof resetAnim === "function") { + this.resetAnim = resetAnim; + } else { + this.resetAnim = undefined; + } + + this.setAnimationFrame(this.current.idx); + + if (!_preserve_dt) { + this.dt = 0; + } + } else { + throw new Error("animation id '" + name + "' not defined"); + } + + return this; + }, + + /** + * reverse the given or current animation if none is specified + * @name reverseAnimation + * @memberOf me.Sprite.prototype + * @function + * @param {String} [name] animation id + * @return {me.Sprite} Reference to this object for method chaining + * @see me.Sprite#animationspeed + */ + reverseAnimation: function reverseAnimation(name) { + if (typeof name !== "undefined" && typeof this.anim[name] !== "undefined") { + this.anim[name].frames.reverse(); + } else { + this.anim[this.current.name].frames.reverse(); + } + + return this; + }, + + /** + * return true if the specified animation is the current one. + * @name isCurrentAnimation + * @memberOf me.Sprite.prototype + * @function + * @param {String} name animation id + * @return {Boolean} + * @example + * if (!this.isCurrentAnimation("walk")) { + * // do something funny... + * } + */ + isCurrentAnimation: function isCurrentAnimation(name) { + return this.current.name === name; + }, + + /** + * change the current texture atlas region for this sprite + * @see me.Texture.getRegion + * @name setRegion + * @memberOf me.Sprite.prototype + * @function + * @param {Object} region typically returned through me.Texture.getRegion() + * @return {me.Sprite} Reference to this object for method chaining + * @example + * // change the sprite to "shadedDark13.png"; + * mySprite.setRegion(game.texture.getRegion("shadedDark13.png")); + */ + setRegion: function setRegion(region) { + if (this.source !== null) { + // set the source texture for the given region + this.image = this.source.getTexture(region); + } // set the sprite offset within the texture + + + this.current.offset.setV(region.offset); // set angle if defined + + this.current.angle = region.angle; // update the default "current" size + + this.width = this.current.width = region.width; + this.height = this.current.height = region.height; // set global anchortPoint if defined + + if (region.anchorPoint) { + this.anchorPoint.set(this._flip.x && region.trimmed === true ? 1 - region.anchorPoint.x : region.anchorPoint.x, this._flip.y && region.trimmed === true ? 1 - region.anchorPoint.y : region.anchorPoint.y); + } + + return this; + }, + + /** + * force the current animation frame index. + * @name setAnimationFrame + * @memberOf me.Sprite.prototype + * @function + * @param {Number} [index=0] animation frame index + * @return {me.Sprite} Reference to this object for method chaining + * @example + * // reset the current animation to the first frame + * this.setAnimationFrame(); + */ + setAnimationFrame: function setAnimationFrame(idx) { + this.current.idx = (idx || 0) % this.current.length; + return this.setRegion(this.getAnimationFrameObjectByIndex(this.current.idx)); + }, + + /** + * return the current animation frame index. + * @name getCurrentAnimationFrame + * @memberOf me.Sprite.prototype + * @function + * @return {Number} current animation frame index + */ + getCurrentAnimationFrame: function getCurrentAnimationFrame() { + return this.current.idx; + }, + + /** + * Returns the frame object by the index. + * @name getAnimationFrameObjectByIndex + * @memberOf me.Sprite.prototype + * @function + * @private + * @return {Number} if using number indices. Returns {Object} containing frame data if using texture atlas + */ + getAnimationFrameObjectByIndex: function getAnimationFrameObjectByIndex(id) { + return this.anim[this.current.name].frames[id]; + }, + + /** + * @ignore + */ + update: function update(dt) { + var result = false; // Update animation if necessary + + if (!this.animationpause && this.current && this.current.length > 0) { + var duration = this.getAnimationFrameObjectByIndex(this.current.idx).delay; + this.dt += dt; + + while (this.dt >= duration) { + result = true; + this.dt -= duration; + var nextFrame = this.current.length > 1 ? this.current.idx + 1 : this.current.idx; + this.setAnimationFrame(nextFrame); // Switch animation if we reach the end of the strip and a callback is defined + + if (this.current.idx === 0 && typeof this.resetAnim === "function") { + // Otherwise is must be callable + if (this.resetAnim() === false) { + // Reset to last frame + this.setAnimationFrame(this.current.length - 1); // Bail early without skipping any more frames. + + this.dt %= duration; + break; + } + } // Get next frame duration + + + duration = this.getAnimationFrameObjectByIndex(this.current.idx).delay; + } + } //update the "flickering" state if necessary + + + if (this._flicker.isFlickering) { + this._flicker.duration -= dt; + + if (this._flicker.duration < 0) { + if (typeof this._flicker.callback === "function") { + this._flicker.callback(); + } + + this.flicker(-1); + } + + result = true; + } + + return result; + }, + + /** + * update the renderable's bounding rect (private) + * @ignore + * @name updateBoundsPos + * @memberOf me.Sprite.prototype + * @function + */ + updateBoundsPos: function updateBoundsPos(newX, newY) { + var bounds = this.getBounds(); + bounds.pos.set(newX - this.anchorPoint.x * bounds.width, newY - this.anchorPoint.y * bounds.height); // XXX: This is called from the constructor, before it gets an ancestor + + if (this.ancestor instanceof me.Container && !this.floating) { + bounds.pos.add(this.ancestor._absPos); + } + + return bounds; + }, + + /** + * called when the anchor point value is changed + * @private + * @name onAnchorUpdate + * @memberOf me.Sprite.prototype + * @function + */ + onAnchorUpdate: function onAnchorUpdate(newX, newY) { + // since the callback is called before setting the new value + // manually update the anchor point (required for updateBoundsPos) + this.anchorPoint.setMuted(newX, newY); // then call updateBouds + + this.updateBoundsPos(this.pos.x, this.pos.y); + }, + + /** + * Destroy function
+ * @ignore + */ + destroy: function destroy() { + me.pool.push(this.offset); + this.offset = undefined; + + this._super(me.Renderable, "destroy"); + }, + + /** + * @ignore + */ + draw: function draw(renderer) { + // do nothing if we are flickering + if (this._flicker.isFlickering) { + this._flicker.state = !this._flicker.state; + + if (!this._flicker.state) { + return; + } + } // the frame to draw + + + var frame = this.current; // cache the current position and size + + var xpos = this.pos.x, + ypos = this.pos.y; + var w = frame.width, + h = frame.height; // frame offset in the texture/atlas + + var frame_offset = frame.offset; + var g_offset = this.offset; // remove image's TexturePacker/ShoeBox rotation + + if (frame.angle !== 0) { + renderer.translate(-xpos, -ypos); + renderer.rotate(frame.angle); + xpos -= h; + w = frame.height; + h = frame.width; + } + + renderer.drawImage(this.image, g_offset.x + frame_offset.x, // sx + g_offset.y + frame_offset.y, // sy + w, h, // sw,sh + xpos, ypos, // dx,dy + w, h // dw,dh + ); + } + }); + })(); + + (function () { + /** + * GUI Object
+ * A very basic object to manage GUI elements
+ * The object simply register on the "pointerdown"
+ * or "touchstart" event and call the onClick function" + * @class + * @extends me.Sprite + * @memberOf me + * @constructor + * @param {Number} x the x coordinate of the GUI Object + * @param {Number} y the y coordinate of the GUI Object + * @param {Object} settings See {@link me.Sprite} + * @example + * + * // create a basic GUI Object + * var myButton = me.GUI_Object.extend( + * { + * init:function (x, y) + * { + * var settings = {} + * settings.image = "button"; + * settings.framewidth = 100; + * settings.frameheight = 50; + * // super constructor + * this._super(me.GUI_Object, "init", [x, y, settings]); + * // define the object z order + * this.pos.z = 4; + * }, + * + * // output something in the console + * // when the object is clicked + * onClick:function (event) + * { + * console.log("clicked!"); + * // don't propagate the event + * return false; + * } + * }); + * + * // add the object at pos (10,10) + * me.game.world.addChild(new myButton(10,10)); + * + */ + me.GUI_Object = me.Sprite.extend({ + /** + * @ignore + */ + init: function init(x, y, settings) { + /** + * object can be clicked or not + * @public + * @type boolean + * @default true + * @name me.GUI_Object#isClickable + */ + this.isClickable = true; + /** + * Tap and hold threshold timeout in ms + * @type {number} + * @default 250 + * @name me.GUI_Object#holdThreshold + */ + + this.holdThreshold = 250; + /** + * object can be tap and hold + * @public + * @type boolean + * @default false + * @name me.GUI_Object#isHoldable + */ + + this.isHoldable = false; + /** + * true if the pointer is over the object + * @public + * @type boolean + * @default false + * @name me.GUI_Object#hover + */ + + this.hover = false; // object has been updated (clicked,etc..) + + this.holdTimeout = null; + this.updated = false; + this.released = true; // call the parent constructor + + this._super(me.Sprite, "init", [x, y, settings]); // GUI items use screen coordinates + + + this.floating = true; // enable event detection + + this.isKinematic = false; + }, + + /** + * return true if the object has been clicked + * @ignore + */ + update: function update(dt) { + // call the parent constructor + var updated = this._super(me.Sprite, "update", [dt]); // check if the button was updated + + + if (this.updated) { + // clear the flag + if (!this.released) { + this.updated = false; + } + + return true; + } // else only return true/false based on the parent function + + + return updated; + }, + + /** + * function callback for the pointerdown event + * @ignore + */ + clicked: function clicked(event) { + // Check if left mouse button is pressed + if (event.button === 0 && this.isClickable) { + this.updated = true; + this.released = false; + + if (this.isHoldable) { + if (this.holdTimeout !== null) { + me.timer.clearTimeout(this.holdTimeout); + } + + this.holdTimeout = me.timer.setTimeout(this.hold.bind(this), this.holdThreshold, false); + this.released = false; + } + + return this.onClick.call(this, event); + } + }, + + /** + * function called when the object is pressed
+ * to be extended
+ * return false if we need to stop propagating the event + * @name onClick + * @memberOf me.GUI_Object.prototype + * @public + * @function + * @param {Event} event the event object + */ + onClick: function onClick() + /* event */ + { + return false; + }, + + /** + * function callback for the pointerEnter event + * @ignore + */ + enter: function enter(event) { + this.hover = true; + return this.onOver.call(this, event); + }, + + /** + * function called when the pointer is over the object + * @name onOver + * @memberOf me.GUI_Object.prototype + * @public + * @function + * @param {Event} event the event object + */ + onOver: function onOver() + /* event */ + {}, + + /** + * function callback for the pointerLeave event + * @ignore + */ + leave: function leave(event) { + this.hover = false; + this.release.call(this, event); + return this.onOut.call(this, event); + }, + + /** + * function called when the pointer is leaving the object area + * @name onOut + * @memberOf me.GUI_Object.prototype + * @public + * @function + * @param {Event} event the event object + */ + onOut: function onOut() + /* event */ + {}, + + /** + * function callback for the pointerup event + * @ignore + */ + release: function release(event) { + if (this.released === false) { + this.released = true; + me.timer.clearTimeout(this.holdTimeout); + return this.onRelease.call(this, event); + } + }, + + /** + * function called when the object is pressed and released
+ * to be extended
+ * return false if we need to stop propagating the event + * @name onRelease + * @memberOf me.GUI_Object.prototype + * @public + * @function + * @param {Event} event the event object + */ + onRelease: function onRelease() { + return false; + }, + + /** + * function callback for the tap and hold timer event + * @ignore + */ + hold: function hold() { + me.timer.clearTimeout(this.holdTimeout); + + if (!this.released) { + this.onHold.call(this); + } + }, + + /** + * function called when the object is pressed and held
+ * to be extended
+ * @name onHold + * @memberOf me.GUI_Object.prototype + * @public + * @function + */ + onHold: function onHold() {}, + + /** + * function called when added to the game world or a container + * @ignore + */ + onActivateEvent: function onActivateEvent() { + // register pointer events + me.input.registerPointerEvent("pointerdown", this, this.clicked.bind(this)); + me.input.registerPointerEvent("pointerup", this, this.release.bind(this)); + me.input.registerPointerEvent("pointercancel", this, this.release.bind(this)); + me.input.registerPointerEvent("pointerenter", this, this.enter.bind(this)); + me.input.registerPointerEvent("pointerleave", this, this.leave.bind(this)); + }, + + /** + * function called when removed from the game world or a container + * @ignore + */ + onDeactivateEvent: function onDeactivateEvent() { + // release pointer events + me.input.releasePointerEvent("pointerdown", this); + me.input.releasePointerEvent("pointerup", this); + me.input.releasePointerEvent("pointercancel", this); + me.input.releasePointerEvent("pointerenter", this); + me.input.releasePointerEvent("pointerleave", this); + me.timer.clearTimeout(this.holdTimeout); + } + }); + })(); + + (function () { + /** + * Private function to re-use for object removal in a defer + * @ignore + */ + var deferredRemove = function deferredRemove(child, keepalive) { + this.removeChildNow(child, keepalive); + }; + + var globalFloatingCounter = 0; + /** + * me.Container represents a collection of child objects + * @class + * @extends me.Renderable + * @memberOf me + * @constructor + * @param {Number} [x=0] position of the container (accessible via the inherited pos.x property) + * @param {Number} [y=0] position of the container (accessible via the inherited pos.y property) + * @param {Number} [w=me.game.viewport.width] width of the container + * @param {Number} [h=me.game.viewport.height] height of the container + */ + + me.Container = me.Renderable.extend({ + /** + * @ignore + */ + init: function init(x, y, width, height, root) { + /** + * keep track of pending sort + * @ignore + */ + this.pendingSort = null; // call the _super constructor + + this._super(me.Renderable, "init", [x || 0, y || 0, width || Infinity, height || Infinity]); + /** + * whether the container is the root of the scene + * @public + * @type Boolean + * @default false + * @name root + * @memberOf me.Container + */ + + + this.root = root || false; + /** + * The array of children of this container. + * @ignore + */ + + this.children = []; + /** + * The property of the child object that should be used to sort on
+ * value : "x", "y", "z" + * @public + * @type String + * @default me.game.sortOn + * @name sortOn + * @memberOf me.Container + */ + + this.sortOn = me.game.sortOn; + /** + * Specify if the children list should be automatically sorted when adding a new child + * @public + * @type Boolean + * @default true + * @name autoSort + * @memberOf me.Container + */ + + this.autoSort = true; + /** + * Specify if the children z index should automatically be managed by the parent container + * @public + * @type Boolean + * @default true + * @name autoDepth + * @memberOf me.Container + */ + + this.autoDepth = true; + /** + * Specify if the container draw operation should clip his children to its own bounds + * @public + * @type Boolean + * @default false + * @name clipping + * @memberOf me.Container + */ + + this.clipping = false; + /** + * a callback to be extended, triggered when a child is added or removed + * @name onChildChange + * @memberOf me.Container# + * @function + * @param {Number} index added or removed child index + */ + + this.onChildChange = function () + /* index */ + {// to be extended + }; + /** + * Used by the debug panel plugin + * @ignore + */ + + + this.drawCount = 0; + /** + * The bounds that contains all its children + * @public + * @type me.Rect + * @name childBounds + * @memberOf me.Container# + */ + + this.childBounds = this.getBounds().clone(); // container self apply any defined transformation + + this.autoTransform = true; // enable collision and event detection + + this.isKinematic = false; // subscribe on the canvas resize event + + if (this.root === true) { + // XXX: Workaround for not updating container child-bounds automatically (it's expensive!) + me.event.subscribe(me.event.CANVAS_ONRESIZE, this.updateChildBounds.bind(this)); + } + }, + + /** + * reset the container, removing all childrens, and reseting transforms. + * @name reset + * @memberOf me.Container + * @function + */ + reset: function reset() { + // cancel any sort operation + if (this.pendingSort) { + clearTimeout(this.pendingSort); + this.pendingSort = null; + } // delete all children + + + for (var i = this.children.length, obj; i >= 0; obj = this.children[--i]) { + // don't remove it if a persistent object + if (obj && !obj.isPersistent) { + this.removeChildNow(obj); + } + } + + if (typeof this.currentTransform !== "undefined") { + // just reset some variables + this.currentTransform.identity(); + } + }, + + /** + * Add a child to the container
+ * if auto-sort is disable, the object will be appended at the bottom of the list. + * Adding a child to the container will automatically remove it from its other container. + * Meaning a child can only have one parent. This is important if you add a renderable + * to a container then add it to the me.game.world container it will move it out of the + * orginal container. Then when the me.game.world.reset() is called the renderable + * will not be in any container. + * @name addChild + * @memberOf me.Container.prototype + * @function + * @param {me.Renderable} child + * @param {number} [z] forces the z index of the child to the specified value + * @return {me.Renderable} the added child + */ + addChild: function addChild(child, z) { + if (child.ancestor instanceof me.Container) { + child.ancestor.removeChildNow(child); + } else { + // only allocate a GUID if the object has no previous ancestor + // (e.g. move one child from one container to another) + if (child.isRenderable) { + // allocated a GUID value (use child.id as based index if defined) + child.GUID = me.utils.createGUID(child.id); + } + } + + child.ancestor = this; + this.children.push(child); // set the child z value if required + + if (typeof child.pos !== "undefined") { + if (typeof z === "number") { + child.pos.z = z; + } else if (this.autoDepth === true) { + child.pos.z = this.children.length; + } + } + + if (this.autoSort === true) { + this.sort(); + } + + if (typeof child.onActivateEvent === "function" && this.isAttachedToRoot()) { + child.onActivateEvent(); + } + + this.onChildChange.call(this, this.children.length - 1); + return child; + }, + + /** + * Add a child to the container at the specified index
+ * (the list won't be sorted after insertion) + * @name addChildAt + * @memberOf me.Container.prototype + * @function + * @param {me.Renderable} child + * @param {Number} index + * @return {me.Renderable} the added child + */ + addChildAt: function addChildAt(child, index) { + if (index >= 0 && index < this.children.length) { + if (child.ancestor instanceof me.Container) { + child.ancestor.removeChildNow(child); + } else { + // only allocate a GUID if the object has no previous ancestor + // (e.g. move one child from one container to another) + if (child.isRenderable) { + // allocated a GUID value + child.GUID = me.utils.createGUID(); + } + } + + child.ancestor = this; + this.children.splice(index, 0, child); + + if (typeof child.onActivateEvent === "function" && this.isAttachedToRoot()) { + child.onActivateEvent(); + } + + this.onChildChange.call(this, index); + return child; + } else { + throw new Error("Index (" + index + ") Out Of Bounds for addChildAt()"); + } + }, + + /** + * The forEach() method executes a provided function once per child element.
+ * callback is invoked with three arguments:
+ * - the element value
+ * - the element index
+ * - the array being traversed
+ * @name forEach + * @memberOf me.Container.prototype + * @function + * @param {Function} callback + * @param {Object} [thisArg] value to use as this(i.e reference Object) when executing callback. + * @example + * // iterate through all children of the root container + * me.game.world.forEach(function (child) { + * // do something with the child + * }); + */ + forEach: function forEach(callback, thisArg) { + var context = this, + i = 0; + var len = this.children.length; + + if (typeof callback !== "function") { + throw new Error(callback + " is not a function"); + } + + if (arguments.length > 1) { + context = thisArg; + } + + while (i < len) { + callback.call(context, this.children[i], i, this.children); + i++; + } + }, + + /** + * Swaps the position (z-index) of 2 children + * @name swapChildren + * @memberOf me.Container.prototype + * @function + * @param {me.Renderable} child + * @param {me.Renderable} child2 + */ + swapChildren: function swapChildren(child, child2) { + var index = this.getChildIndex(child); + var index2 = this.getChildIndex(child2); + + if (index !== -1 && index2 !== -1) { + // swap z index + var _z = child.pos.z; + child.pos.z = child2.pos.z; + child2.pos.z = _z; // swap the positions.. + + this.children[index] = child2; + this.children[index2] = child; + } else { + throw new Error(child + " Both the supplied childs must be a child of the caller " + this); + } + }, + + /** + * Returns the Child at the specified index + * @name getChildAt + * @memberOf me.Container.prototype + * @function + * @param {Number} index + */ + getChildAt: function getChildAt(index) { + if (index >= 0 && index < this.children.length) { + return this.children[index]; + } else { + throw new Error("Index (" + index + ") Out Of Bounds for getChildAt()"); + } + }, + + /** + * Returns the index of the given Child + * @name getChildIndex + * @memberOf me.Container.prototype + * @function + * @param {me.Renderable} child + */ + getChildIndex: function getChildIndex(child) { + return this.children.indexOf(child); + }, + + /** + * Returns the next child within the container or undefined if none + * @name getNextChild + * @memberOf me.Container + * @function + * @param {me.Renderable} child + */ + getNextChild: function getNextChild(child) { + var index = this.children.indexOf(child) - 1; + + if (index >= 0 && index < this.children.length) { + return this.getChildAt(index); + } + + return undefined; + }, + + /** + * Returns true if contains the specified Child + * @name hasChild + * @memberOf me.Container.prototype + * @function + * @param {me.Renderable} child + * @return {Boolean} + */ + hasChild: function hasChild(child) { + return this === child.ancestor; + }, + + /** + * return the child corresponding to the given property and value.
+ * note : avoid calling this function every frame since + * it parses the whole object tree each time + * @name getChildByProp + * @memberOf me.Container.prototype + * @public + * @function + * @param {String} prop Property name + * @param {String|RegExp|Number|Boolean} value Value of the property + * @return {me.Renderable[]} Array of childs + * @example + * // get the first child object called "mainPlayer" in a specific container : + * var ent = myContainer.getChildByProp("name", "mainPlayer"); + * + * // or query the whole world : + * var ent = me.game.world.getChildByProp("name", "mainPlayer"); + * + * // partial property matches are also allowed by using a RegExp. + * // the following matches "redCOIN", "bluecoin", "bagOfCoins", etc : + * var allCoins = me.game.world.getChildByProp("name", /coin/i); + * + * // searching for numbers or other data types : + * var zIndex10 = me.game.world.getChildByProp("z", 10); + * var inViewport = me.game.world.getChildByProp("inViewport", true); + */ + getChildByProp: function getChildByProp(prop, value) { + var objList = []; + + function compare(obj, prop) { + var v = obj[prop]; + + if (value instanceof RegExp && typeof v === "string") { + if (value.test(v)) { + objList.push(obj); + } + } else if (v === value) { + objList.push(obj); + } + } + + for (var i = this.children.length - 1; i >= 0; i--) { + var obj = this.children[i]; + compare(obj, prop); + + if (obj instanceof me.Container) { + objList = objList.concat(obj.getChildByProp(prop, value)); + } + } + + return objList; + }, + + /** + * returns the list of childs with the specified class type + * @name getChildByType + * @memberOf me.Container.prototype + * @public + * @function + * @param {Object} class type + * @return {me.Renderable[]} Array of children + */ + getChildByType: function getChildByType(_class) { + var objList = []; + + for (var i = this.children.length - 1; i >= 0; i--) { + var obj = this.children[i]; + + if (obj instanceof _class) { + objList.push(obj); + } + + if (obj instanceof me.Container) { + objList = objList.concat(obj.getChildByType(_class)); + } + } + + return objList; + }, + + /** + * returns the list of childs with the specified name
+ * as defined in Tiled (Name field of the Object Properties)
+ * note : avoid calling this function every frame since + * it parses the whole object list each time + * @name getChildByName + * @memberOf me.Container.prototype + * @public + * @function + * @param {String|RegExp|Number|Boolean} name entity name + * @return {me.Renderable[]} Array of children + */ + getChildByName: function getChildByName(name) { + return this.getChildByProp("name", name); + }, + + /** + * return the child corresponding to the specified GUID
+ * note : avoid calling this function every frame since + * it parses the whole object list each time + * @name getChildByGUID + * @memberOf me.Container.prototype + * @public + * @function + * @param {String|RegExp|Number|Boolean} GUID entity GUID + * @return {me.Renderable} corresponding child or null + */ + getChildByGUID: function getChildByGUID(guid) { + var obj = this.getChildByProp("GUID", guid); + return obj.length > 0 ? obj[0] : null; + }, + + /** + * resizes the child bounds rectangle, based on children bounds. + * @name updateChildBounds + * @memberOf me.Container.prototype + * @function + * @return {me.Rect} updated child bounds + */ + updateChildBounds: function updateChildBounds() { + this.childBounds.pos.set(Infinity, Infinity); + this.childBounds.resize(-Infinity, -Infinity); + var childBounds; + + for (var i = this.children.length, child; i--, child = this.children[i];) { + if (child.isRenderable) { + if (child instanceof me.Container) { + childBounds = child.childBounds; + } else { + childBounds = child.getBounds(); + } // TODO : returns an "empty" rect instead of null (e.g. EntityObject) + // TODO : getBounds should always return something anyway + + + if (childBounds !== null) { + this.childBounds.union(childBounds); + } + } + } + + return this.childBounds; + }, + + /** + * Checks if this container is root or if it's attached to the root container. + * @private + * @name isAttachedToRoot + * @memberOf me.Container.prototype + * @function + * @returns Boolean + */ + isAttachedToRoot: function isAttachedToRoot() { + if (this.root === true) { + return true; + } else { + var ancestor = this.ancestor; + + while (ancestor) { + if (ancestor.root === true) { + return true; + } + + ancestor = ancestor.ancestor; + } + + return false; + } + }, + + /** + * update the renderable's bounding rect (private) + * @private + * @name updateBoundsPos + * @memberOf me.Container.prototype + * @function + */ + updateBoundsPos: function updateBoundsPos(newX, newY) { + this._super(me.Renderable, "updateBoundsPos", [newX, newY]); // Update container's absolute position + + + this._absPos.set(newX, newY); + + if (this.ancestor instanceof me.Container && !this.floating) { + this._absPos.add(this.ancestor._absPos); + } // Notify children that the parent's position has changed + + + for (var i = this.children.length, child; i--, child = this.children[i];) { + if (child.isRenderable) { + child.updateBoundsPos(child.pos.x, child.pos.y); + } + } + + return this.getBounds(); + }, + + /** + * @ignore + */ + onActivateEvent: function onActivateEvent() { + for (var i = this.children.length, child; i--, child = this.children[i];) { + if (typeof child.onActivateEvent === "function") { + child.onActivateEvent(); + } + } + }, + + /** + * Invokes the removeChildNow in a defer, to ensure the child is removed safely after the update & draw stack has completed + * @name removeChild + * @memberOf me.Container.prototype + * @public + * @function + * @param {me.Renderable} child + * @param {Boolean} [keepalive=False] True to prevent calling child.destroy() + */ + removeChild: function removeChild(child, keepalive) { + if (this.hasChild(child)) { + me.utils.function.defer(deferredRemove, this, child, keepalive); + } else { + throw new Error("Child is not mine."); + } + }, + + /** + * Removes (and optionally destroys) a child from the container.
+ * (removal is immediate and unconditional)
+ * Never use keepalive=true with objects from {@link me.pool}. Doing so will create a memory leak. + * @name removeChildNow + * @memberOf me.Container.prototype + * @function + * @param {me.Renderable} child + * @param {Boolean} [keepalive=False] True to prevent calling child.destroy() + */ + removeChildNow: function removeChildNow(child, keepalive) { + if (this.hasChild(child) && this.getChildIndex(child) >= 0) { + if (typeof child.onDeactivateEvent === "function") { + child.onDeactivateEvent(); + } + + if (!keepalive) { + if (typeof child.destroy === "function") { + child.destroy(); + } + + me.pool.push(child); + } // Don't cache the child index; another element might have been removed + // by the child's `onDeactivateEvent` or `destroy` methods + + + var childIndex = this.getChildIndex(child); + + if (childIndex >= 0) { + this.children.splice(childIndex, 1); + child.ancestor = undefined; + } + + this.onChildChange.call(this, childIndex); + } + }, + + /** + * Automatically set the specified property of all childs to the given value + * @name setChildsProperty + * @memberOf me.Container.prototype + * @function + * @param {String} property property name + * @param {Object} value property value + * @param {Boolean} [recursive=false] recursively apply the value to child containers if true + */ + setChildsProperty: function setChildsProperty(prop, val, recursive) { + for (var i = this.children.length; i >= 0; i--) { + var obj = this.children[i]; + + if (recursive === true && obj instanceof me.Container) { + obj.setChildsProperty(prop, val, recursive); + } + + obj[prop] = val; + } + }, + + /** + * Move the child in the group one step forward (z depth). + * @name moveUp + * @memberOf me.Container.prototype + * @function + * @param {me.Renderable} child + */ + moveUp: function moveUp(child) { + var childIndex = this.getChildIndex(child); + + if (childIndex - 1 >= 0) { + // note : we use an inverted loop + this.swapChildren(child, this.getChildAt(childIndex - 1)); + } + }, + + /** + * Move the child in the group one step backward (z depth). + * @name moveDown + * @memberOf me.Container.prototype + * @function + * @param {me.Renderable} child + */ + moveDown: function moveDown(child) { + var childIndex = this.getChildIndex(child); + + if (childIndex >= 0 && childIndex + 1 < this.children.length) { + // note : we use an inverted loop + this.swapChildren(child, this.getChildAt(childIndex + 1)); + } + }, + + /** + * Move the specified child to the top(z depth). + * @name moveToTop + * @memberOf me.Container.prototype + * @function + * @param {me.Renderable} child + */ + moveToTop: function moveToTop(child) { + var childIndex = this.getChildIndex(child); + + if (childIndex > 0) { + // note : we use an inverted loop + this.children.splice(0, 0, this.children.splice(childIndex, 1)[0]); // increment our child z value based on the previous child depth + + child.pos.z = this.children[1].pos.z + 1; + } + }, + + /** + * Move the specified child the bottom (z depth). + * @name moveToBottom + * @memberOf me.Container.prototype + * @function + * @param {me.Renderable} child + */ + moveToBottom: function moveToBottom(child) { + var childIndex = this.getChildIndex(child); + + if (childIndex >= 0 && childIndex < this.children.length - 1) { + // note : we use an inverted loop + this.children.splice(this.children.length - 1, 0, this.children.splice(childIndex, 1)[0]); // increment our child z value based on the next child depth + + child.pos.z = this.children[this.children.length - 2].pos.z - 1; + } + }, + + /** + * Manually trigger the sort of all the childs in the container

+ * @name sort + * @memberOf me.Container.prototype + * @public + * @function + * @param {Boolean} [recursive=false] recursively sort all containers if true + */ + sort: function sort(recursive) { + // do nothing if there is already a pending sort + if (!this.pendingSort) { + if (recursive === true) { + // trigger other child container sort function (if any) + for (var i = this.children.length, obj; i--, obj = this.children[i];) { + if (obj instanceof me.Container) { + // note : this will generate one defered sorting function + // for each existing containe + obj.sort(recursive); + } + } + } + /** @ignore */ + + + this.pendingSort = me.utils.function.defer(function (self) { + // sort everything in this container + self.children.sort(self["_sort" + self.sortOn.toUpperCase()]); // clear the defer id + + self.pendingSort = null; // make sure we redraw everything + + me.game.repaint(); + }, this, this); + } + }, + + /** + * @ignore + */ + onDeactivateEvent: function onDeactivateEvent() { + for (var i = this.children.length, child; i--, child = this.children[i];) { + if (typeof child.onDeactivateEvent === "function") { + child.onDeactivateEvent(); + } + } + }, + + /** + * Z Sorting function + * @ignore + */ + _sortZ: function _sortZ(a, b) { + return b.pos && a.pos ? b.pos.z - a.pos.z : a.pos ? -Infinity : Infinity; + }, + + /** + * Reverse Z Sorting function + * @ignore + */ + _sortReverseZ: function _sortReverseZ(a, b) { + return a.pos && b.pos ? a.pos.z - b.pos.z : a.pos ? Infinity : -Infinity; + }, + + /** + * X Sorting function + * @ignore + */ + _sortX: function _sortX(a, b) { + if (!b.pos || !a.pos) { + return a.pos ? -Infinity : Infinity; + } + + var result = b.pos.z - a.pos.z; + return result ? result : b.pos.x - a.pos.x; + }, + + /** + * Y Sorting function + * @ignore + */ + _sortY: function _sortY(a, b) { + if (!b.pos || !a.pos) { + return a.pos ? -Infinity : Infinity; + } + + var result = b.pos.z - a.pos.z; + return result ? result : b.pos.y - a.pos.y; + }, + + /** + * Destroy function
+ * @ignore + */ + destroy: function destroy() { + // empty the container + this.reset(); // call the parent destroy method + + this._super(me.Renderable, "destroy", arguments); + }, + + /** + * @ignore + */ + update: function update(dt) { + this._super(me.Renderable, "update", [dt]); + + var isDirty = false; + var isFloating = false; + var isPaused = me.state.isPaused(); // Update container's absolute position + + this._absPos.setV(this.pos); + + if (this.ancestor) { + this._absPos.add(this.ancestor._absPos); + } + + for (var i = this.children.length, obj; i--, obj = this.children[i];) { + if (isPaused && !obj.updateWhenPaused) { + // skip this object + continue; + } + + if (obj.isRenderable) { + isFloating = globalFloatingCounter > 0 || obj.floating; + + if (isFloating) { + globalFloatingCounter++; + } // check if object is in any active cameras + + + obj.inViewport = false; // iterate through all cameras + + me.state.current().cameras.forEach(function (camera) { + if (camera.isVisible(obj, isFloating)) { + obj.inViewport = true; + } + }); // update our object + + isDirty = (obj.inViewport || obj.alwaysUpdate) && obj.update(dt) || isDirty; // Update child's absolute position + + obj._absPos.setV(this._absPos).add(obj.pos); + + if (globalFloatingCounter > 0) { + globalFloatingCounter--; + } + } else { + // just directly call update() for non renderable object + isDirty = obj.update(dt) || isDirty; + } + } + + return isDirty; + }, + + /** + * @ignore + */ + draw: function draw(renderer, rect) { + var isFloating = false; + this.drawCount = 0; // clip the containter children to the container bounds + + if (this.root === false && this.clipping === true && this.childBounds.isFinite() === true) { + renderer.clipRect(this.childBounds.pos.x, this.childBounds.pos.y, this.childBounds.width, this.childBounds.height); + } // adjust position if required (e.g. canvas/window centering) + + + renderer.translate(this.pos.x, this.pos.y); + + for (var i = this.children.length, obj; i--, obj = this.children[i];) { + if (obj.isRenderable) { + isFloating = obj.floating === true; + + if (obj.inViewport || isFloating) { + if (isFloating) { + // translate to screen coordinates + renderer.save(); + renderer.resetTransform(); + } // predraw (apply transforms) + + + obj.preDraw(renderer); // draw the object + + obj.draw(renderer, rect); // postdraw (clean-up); + + obj.postDraw(renderer); // restore the previous "state" + + if (isFloating) { + renderer.restore(); + } + + this.drawCount++; + } + } + } + } + }); + })(); + + (function () { + // some ref shortcut + var MIN = Math.min, + MAX = Math.max; + var targetV = new me.Vector2d(); + /** + * a 2D orthographic camera + * @class + * @extends me.Renderable + * @memberOf me + * @constructor + * @param {Number} minX start x offset + * @param {Number} minY start y offset + * @param {Number} maxX end x offset + * @param {Number} maxY end y offset + */ + + me.Camera2d = me.Renderable.extend({ + /** + * @ignore + */ + init: function init(minX, minY, maxX, maxY) { + this._super(me.Renderable, "init", [minX, minY, maxX - minX, maxY - minY]); + /** + * Axis definition + * @property NONE + * @property HORIZONTAL + * @property VERTICAL + * @property BOTH + * @public + * @constant + * @enum {Number} + * @name AXIS + * @memberOf me.Camera2d + */ + + + this.AXIS = { + NONE: 0, + HORIZONTAL: 1, + VERTICAL: 2, + BOTH: 3 + }; + /** + * Camera bounds + * @public + * @type me.Rect + * @name bounds + * @memberOf me.Camera2d + */ + + this.bounds = new me.Rect(-Infinity, -Infinity, Infinity, Infinity); + /** + * [IMTERNAL] enable or disable damping + * @private + * @type {Boolean} + * @name smoothFollow + * @see me.Camera2d.damping + * @default true + * @memberOf me.Camera2d + */ + + this.smoothFollow = true; + /** + * Camera damping for smooth transition [0 .. 1]. + * 1 being the maximum value and will snap the camera to the target position + * @public + * @type {Number} + * @name damping + * @default 1.0 + * @memberOf me.Camera2d + */ + + this.damping = 1.0; // offset for shake effect + + this.offset = new me.Vector2d(); // target to follow + + this.target = null; // default value follow + + this.follow_axis = this.AXIS.NONE; // shake variables + + this._shake = { + intensity: 0, + duration: 0, + axis: this.AXIS.BOTH, + onComplete: null + }; // flash variables + + this._fadeOut = { + color: null, + tween: null + }; // fade variables + + this._fadeIn = { + color: null, + tween: null + }; // default camera name + + this.name = "default"; // set a default deadzone + + this.setDeadzone(this.width / 6, this.height / 6); // for backward "compatiblity" (in terms of behavior) + + this.anchorPoint.set(0, 0); // enable event detection on the camera + + this.isKinematic = false; // subscribe to the game reset event + + me.event.subscribe(me.event.GAME_RESET, this.reset.bind(this)); // subscribe to the canvas resize event + + me.event.subscribe(me.event.CANVAS_ONRESIZE, this.resize.bind(this)); + }, + // -- some private function --- + + /** @ignore */ + _followH: function _followH(target) { + var targetX = this.pos.x; + + if (target.x - this.pos.x > this.deadzone.right) { + targetX = MIN(target.x - this.deadzone.right, this.bounds.width - this.width); + } else if (target.x - this.pos.x < this.deadzone.pos.x) { + targetX = MAX(target.x - this.deadzone.pos.x, this.bounds.pos.x); + } + + return targetX; + }, + + /** @ignore */ + _followV: function _followV(target) { + var targetY = this.pos.y; + + if (target.y - this.pos.y > this.deadzone.bottom) { + targetY = MIN(target.y - this.deadzone.bottom, this.bounds.height - this.height); + } else if (target.y - this.pos.y < this.deadzone.pos.y) { + targetY = MAX(target.y - this.deadzone.pos.y, this.bounds.pos.y); + } + + return targetY; + }, + // -- public function --- + + /** + * reset the camera position to specified coordinates + * @name reset + * @memberOf me.Camera2d + * @function + * @param {Number} [x=0] + * @param {Number} [y=0] + */ + reset: function reset(x, y) { + // reset the initial camera position to 0,0 + this.pos.x = x || 0; + this.pos.y = y || 0; // reset the target + + this.unfollow(); // damping default value + + this.smoothFollow = true; + this.damping = 1.0; // reset the transformation matrix + + this.currentTransform.identity(); + }, + + /** + * change the deadzone settings. + * the "deadzone" defines an area within the current camera in which + * the followed renderable can move without scrolling the camera. + * @name setDeadzone + * @see me.Camera2d.follow + * @memberOf me.Camera2d + * @function + * @param {Number} w deadzone width + * @param {Number} h deadzone height + */ + setDeadzone: function setDeadzone(w, h) { + if (typeof this.deadzone === "undefined") { + this.deadzone = new me.Rect(0, 0, 0, 0); + } // reusing the old code for now... + + + this.deadzone.pos.set(~~((this.width - w) / 2), ~~((this.height - h) / 2 - h * 0.25)); + this.deadzone.resize(w, h); + this.smoothFollow = false; // force a camera update + + this.updateTarget(); + this.smoothFollow = true; + }, + + /** + * resize the camera + * @name resize + * @memberOf me.Camera2d + * @function + * @param {Number} w new width of the camera + * @param {Number} h new height of the camera + * @return {me.Camera2d} this camera + */ + resize: function resize(w, h) { + // parent consctructor, resize camera rect + this._super(me.Renderable, "resize", [w, h]); // disable damping while resizing + + + this.smoothFollow = false; // update bounds + + var level = me.levelDirector.getCurrentLevel(); + this.setBounds(0, 0, Math.max(w, level ? level.width : 0), Math.max(h, level ? level.height : 0)); // reset everthing + + this.setDeadzone(w / 6, h / 6); + this.update(); + this.smoothFollow = true; + me.event.publish(me.event.VIEWPORT_ONRESIZE, [this.width, this.height]); + return this; + }, + + /** + * set the camera boundaries (set to the world limit by default). + * the camera is bound to the given coordinates and cannot move/be scrolled outside of it. + * @name setBounds + * @memberOf me.Camera2d + * @function + * @param {Number} x world left limit + * @param {Number} y world top limit + * @param {Number} w world width limit + * @param {Number} h world height limit + */ + setBounds: function setBounds(x, y, w, h) { + this.smoothFollow = false; + this.bounds.pos.set(x, y); + this.bounds.resize(w, h); + this.moveTo(this.pos.x, this.pos.y); + this.update(); + this.smoothFollow = true; + }, + + /** + * set the camera to follow the specified renderable.
+ * (this will put the camera center around the given target) + * @name follow + * @memberOf me.Camera2d + * @function + * @param {me.Renderable|me.Vector2d} target renderable or position vector to follow + * @param {me.Camera2d.AXIS} [axis=this.AXIS.BOTH] Which axis to follow + * @param {Number} [damping=1] default damping value + * @example + * // set the camera to follow this renderable on both axis, and enable damping + * me.game.viewport.follow(this, me.game.viewport.AXIS.BOTH, 0.1); + */ + follow: function follow(target, axis, damping) { + if (target instanceof me.Renderable) { + this.target = target.pos; + } else if (target instanceof me.Vector2d || target instanceof me.Vector3d) { + this.target = target; + } else { + throw new Error("invalid target for me.Camera2d.follow"); + } // if axis is null, camera is moved on target center + + + this.follow_axis = typeof axis === "undefined" ? this.AXIS.BOTH : axis; + this.smoothFollow = false; + + if (typeof damping !== "number") { + this.damping = 1; + } else { + this.damping = me.Math.clamp(damping, 0.0, 1.0); + } // force a camera update + + + this.updateTarget(); + this.smoothFollow = true; + }, + + /** + * unfollow the current target + * @name unfollow + * @memberOf me.Camera2d + * @function + */ + unfollow: function unfollow() { + this.target = null; + this.follow_axis = this.AXIS.NONE; + }, + + /** + * move the camera upper-left position by the specified offset. + * @name move + * @memberOf me.Camera2d + * @see me.Camera2d.focusOn + * @function + * @param {Number} x + * @param {Number} y + * @example + * // Move the camera up by four pixels + * me.game.viewport.move(0, -4); + */ + move: function move(x, y) { + this.moveTo(this.pos.x + x, this.pos.y + y); + }, + + /** + * move the camera upper-left position to the specified coordinates + * @name moveTo + * @memberOf me.Camera2d + * @see me.Camera2d.focusOn + * @function + * @param {Number} x + * @param {Number} y + */ + moveTo: function moveTo(x, y) { + var _x = this.pos.x; + var _y = this.pos.y; + this.pos.x = me.Math.clamp(x, this.bounds.pos.x, this.bounds.width - this.width); + this.pos.y = me.Math.clamp(y, this.bounds.pos.y, this.bounds.height - this.height); //publish the VIEWPORT_ONCHANGE event if necessary + + if (_x !== this.pos.x || _y !== this.pos.y) { + me.event.publish(me.event.VIEWPORT_ONCHANGE, [this.pos]); + } + }, + + /** @ignore */ + updateTarget: function updateTarget() { + if (this.target) { + targetV.setV(this.pos); + + switch (this.follow_axis) { + case this.AXIS.NONE: + //this.focusOn(this.target); + break; + + case this.AXIS.HORIZONTAL: + targetV.x = this._followH(this.target); + break; + + case this.AXIS.VERTICAL: + targetV.y = this._followV(this.target); + break; + + case this.AXIS.BOTH: + targetV.x = this._followH(this.target); + targetV.y = this._followV(this.target); + break; + + default: + break; + } + + if (!this.pos.equals(targetV)) { + // update the camera position + if (this.smoothFollow === true && this.damping < 1.0) { + // account for floating precision and check if we are close "enough" + if (me.Math.toBeCloseTo(targetV.x, this.pos.x, 2) && me.Math.toBeCloseTo(targetV.y, this.pos.y, 2)) { + this.pos.setV(targetV); + return false; + } else { + this.pos.lerp(targetV, this.damping); + } + } else { + this.pos.setV(targetV); + } + + return true; + } + } + + return false; + }, + + /** @ignore */ + update: function update(dt) { + var updated = this.updateTarget(dt); + + if (this._shake.duration > 0) { + this._shake.duration -= dt; + + if (this._shake.duration <= 0) { + this._shake.duration = 0; + this.offset.setZero(); + + if (typeof this._shake.onComplete === "function") { + this._shake.onComplete(); + } + } else { + if (this._shake.axis === this.AXIS.BOTH || this._shake.axis === this.AXIS.HORIZONTAL) { + this.offset.x = (Math.random() - 0.5) * this._shake.intensity; + } + + if (this._shake.axis === this.AXIS.BOTH || this._shake.axis === this.AXIS.VERTICAL) { + this.offset.y = (Math.random() - 0.5) * this._shake.intensity; + } + } // updated! + + + updated = true; + } + + if (updated === true) { + //publish the corresponding message + me.event.publish(me.event.VIEWPORT_ONCHANGE, [this.pos]); + } // check for fade/flash effect + + + if (this._fadeIn.tween != null || this._fadeOut.tween != null) { + updated = true; + } + + return updated; + }, + + /** + * shake the camera + * @name shake + * @memberOf me.Camera2d + * @function + * @param {Number} intensity maximum offset that the screen can be moved + * while shaking + * @param {Number} duration expressed in milliseconds + * @param {me.Camera2d.AXIS} [axis=this.AXIS.BOTH] specify on which axis you + * want the shake effect + * @param {Function} [onComplete] callback once shaking effect is over + * @param {Boolean} [force] if true this will override the current effect + * @example + * // shake it baby ! + * me.game.viewport.shake(10, 500, me.game.viewport.AXIS.BOTH); + */ + shake: function shake(intensity, duration, axis, onComplete, force) { + if (this._shake.duration === 0 || force === true) { + this._shake.intensity = intensity; + this._shake.duration = duration; + this._shake.axis = axis || this.AXIS.BOTH; + this._shake.onComplete = typeof onComplete === "function" ? onComplete : undefined; + } + }, + + /** + * fadeOut(flash) effect

+ * 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
+ * @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=0; i--) { + self._howls[i].unload(); + } + + // Create a new AudioContext to make sure it is fully reset. + if (self.usingWebAudio && self.ctx && typeof self.ctx.close !== 'undefined') { + self.ctx.close(); + self.ctx = null; + setupAudioContext(); + } + + return self; + }, + + /** + * Check for codec support of specific extension. + * @param {String} ext Audio file extention. + * @return {Boolean} + */ + codecs: function(ext) { + return (this || Howler)._codecs[ext.replace(/^x-/, '')]; + }, + + /** + * Setup various state values for global tracking. + * @return {Howler} + */ + _setup: function() { + var self = this || Howler; + + // Keeps track of the suspend/resume state of the AudioContext. + self.state = self.ctx ? self.ctx.state || 'suspended' : 'suspended'; + + // Automatically begin the 30-second suspend process + self._autoSuspend(); + + // Check if audio is available. + if (!self.usingWebAudio) { + // No audio is available on this system if noAudio is set to true. + if (typeof Audio !== 'undefined') { + try { + var test = new Audio(); + + // Check if the canplaythrough event is available. + if (typeof test.oncanplaythrough === 'undefined') { + self._canPlayEvent = 'canplay'; + } + } catch(e) { + self.noAudio = true; + } + } else { + self.noAudio = true; + } + } + + // Test to make sure audio isn't disabled in Internet Explorer. + try { + var test = new Audio(); + if (test.muted) { + self.noAudio = true; + } + } catch (e) {} + + // Check for supported codecs. + if (!self.noAudio) { + self._setupCodecs(); + } + + return self; + }, + + /** + * Check for browser support for various codecs and cache the results. + * @return {Howler} + */ + _setupCodecs: function() { + var self = this || Howler; + var audioTest = null; + + // Must wrap in a try/catch because IE11 in server mode throws an error. + try { + audioTest = (typeof Audio !== 'undefined') ? new Audio() : null; + } catch (err) { + return self; + } + + if (!audioTest || typeof audioTest.canPlayType !== 'function') { + return self; + } + + var mpegTest = audioTest.canPlayType('audio/mpeg;').replace(/^no$/, ''); + + // Opera version <33 has mixed MP3 support, so we need to check for and block it. + var checkOpera = self._navigator && self._navigator.userAgent.match(/OPR\/([0-6].)/g); + var isOldOpera = (checkOpera && parseInt(checkOpera[0].split('/')[1], 10) < 33); + + self._codecs = { + mp3: !!(!isOldOpera && (mpegTest || audioTest.canPlayType('audio/mp3;').replace(/^no$/, ''))), + mpeg: !!mpegTest, + opus: !!audioTest.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/, ''), + ogg: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, ''), + oga: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, ''), + wav: !!audioTest.canPlayType('audio/wav; codecs="1"').replace(/^no$/, ''), + aac: !!audioTest.canPlayType('audio/aac;').replace(/^no$/, ''), + caf: !!audioTest.canPlayType('audio/x-caf;').replace(/^no$/, ''), + m4a: !!(audioTest.canPlayType('audio/x-m4a;') || audioTest.canPlayType('audio/m4a;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, ''), + mp4: !!(audioTest.canPlayType('audio/x-mp4;') || audioTest.canPlayType('audio/mp4;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, ''), + weba: !!audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, ''), + webm: !!audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, ''), + dolby: !!audioTest.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/, ''), + flac: !!(audioTest.canPlayType('audio/x-flac;') || audioTest.canPlayType('audio/flac;')).replace(/^no$/, '') + }; + + return self; + }, + + /** + * Some browsers/devices will only allow audio to be played after a user interaction. + * Attempt to automatically unlock audio on the first user interaction. + * Concept from: http://paulbakaus.com/tutorials/html5/web-audio-on-ios/ + * @return {Howler} + */ + _unlockAudio: function() { + var self = this || Howler; + + // Only run this on certain browsers/devices. + var shouldUnlock = /iPhone|iPad|iPod|Android|BlackBerry|BB10|Silk|Mobi|Chrome|Safari/i.test(self._navigator && self._navigator.userAgent); + if (self._audioUnlocked || !self.ctx || !shouldUnlock) { + return; + } + + self._audioUnlocked = false; + self.autoUnlock = false; + + // Some mobile devices/platforms have distortion issues when opening/closing tabs and/or web views. + // Bugs in the browser (especially Mobile Safari) can cause the sampleRate to change from 44100 to 48000. + // By calling Howler.unload(), we create a new AudioContext with the correct sampleRate. + if (!self._mobileUnloaded && self.ctx.sampleRate !== 44100) { + self._mobileUnloaded = true; + self.unload(); + } + + // Scratch buffer for enabling iOS to dispose of web audio buffers correctly, as per: + // http://stackoverflow.com/questions/24119684 + self._scratchBuffer = self.ctx.createBuffer(1, 1, 22050); + + // Call this method on touch start to create and play a buffer, + // then check if the audio actually played to determine if + // audio has now been unlocked on iOS, Android, etc. + var unlock = function(e) { + // Create a pool of unlocked HTML5 Audio objects that can + // be used for playing sounds without user interaction. HTML5 + // Audio objects must be individually unlocked, as opposed + // to the WebAudio API which only needs a single activation. + // This must occur before WebAudio setup or the source.onended + // event will not fire. + for (var i=0; i= 55. + if (typeof self.ctx.resume === 'function') { + self.ctx.resume(); + } + + // Setup a timeout to check that we are unlocked on the next event loop. + source.onended = function() { + source.disconnect(0); + + // Update the unlocked state and prevent this check from happening again. + self._audioUnlocked = true; + + // Remove the touch start listener. + document.removeEventListener('touchstart', unlock, true); + document.removeEventListener('touchend', unlock, true); + document.removeEventListener('click', unlock, true); + + // Let all sounds know that audio has been unlocked. + for (var i=0; i 0 ? sound._seek : self._sprite[sprite][0] / 1000); + var duration = Math.max(0, ((self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000) - seek); + var timeout = (duration * 1000) / Math.abs(sound._rate); + var start = self._sprite[sprite][0] / 1000; + var stop = (self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000; + var loop = !!(sound._loop || self._sprite[sprite][2]); + sound._sprite = sprite; + + // Mark the sound as ended instantly so that this async playback + // doesn't get grabbed by another call to play while this one waits to start. + sound._ended = false; + + // Update the parameters of the sound. + var setParams = function() { + sound._paused = false; + sound._seek = seek; + sound._start = start; + sound._stop = stop; + sound._loop = loop; + }; + + // End the sound instantly if seek is at the end. + if (seek >= stop) { + self._ended(sound); + return; + } + + // Begin the actual playback. + var node = sound._node; + if (self._webAudio) { + // Fire this when the sound is ready to play to begin Web Audio playback. + var playWebAudio = function() { + self._playLock = false; + setParams(); + self._refreshBuffer(sound); + + // Setup the playback params. + var vol = (sound._muted || self._muted) ? 0 : sound._volume; + node.gain.setValueAtTime(vol, Howler.ctx.currentTime); + sound._playStart = Howler.ctx.currentTime; + + // Play the sound using the supported method. + if (typeof node.bufferSource.start === 'undefined') { + sound._loop ? node.bufferSource.noteGrainOn(0, seek, 86400) : node.bufferSource.noteGrainOn(0, seek, duration); + } else { + sound._loop ? node.bufferSource.start(0, seek, 86400) : node.bufferSource.start(0, seek, duration); + } + + // Start a new timer if none is present. + if (timeout !== Infinity) { + self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); + } + + if (!internal) { + setTimeout(function() { + self._emit('play', sound._id); + self._loadQueue(); + }, 0); + } + }; + + if (Howler.state === 'running') { + playWebAudio(); + } else { + self._playLock = true; + + // Wait for the audio context to resume before playing. + self.once('resume', playWebAudio); + + // Cancel the end timer. + self._clearTimer(sound._id); + } + } else { + // Fire this when the sound is ready to play to begin HTML5 Audio playback. + var playHtml5 = function() { + node.currentTime = seek; + node.muted = sound._muted || self._muted || Howler._muted || node.muted; + node.volume = sound._volume * Howler.volume(); + node.playbackRate = sound._rate; + + // Some browsers will throw an error if this is called without user interaction. + try { + var play = node.play(); + + // Support older browsers that don't support promises, and thus don't have this issue. + if (play && typeof Promise !== 'undefined' && (play instanceof Promise || typeof play.then === 'function')) { + // Implements a lock to prevent DOMException: The play() request was interrupted by a call to pause(). + self._playLock = true; + + // Set param values immediately. + setParams(); + + // Releases the lock and executes queued actions. + play + .then(function() { + self._playLock = false; + node._unlocked = true; + if (!internal) { + self._emit('play', sound._id); + self._loadQueue(); + } + }) + .catch(function() { + self._playLock = false; + self._emit('playerror', sound._id, 'Playback was unable to start. This is most commonly an issue ' + + 'on mobile devices and Chrome where playback was not within a user interaction.'); + + // Reset the ended and paused values. + sound._ended = true; + sound._paused = true; + }); + } else if (!internal) { + self._playLock = false; + setParams(); + self._emit('play', sound._id); + self._loadQueue(); + } + + // Setting rate before playing won't work in IE, so we set it again here. + node.playbackRate = sound._rate; + + // If the node is still paused, then we can assume there was a playback issue. + if (node.paused) { + self._emit('playerror', sound._id, 'Playback was unable to start. This is most commonly an issue ' + + 'on mobile devices and Chrome where playback was not within a user interaction.'); + return; + } + + // Setup the end timer on sprites or listen for the ended event. + if (sprite !== '__default' || sound._loop) { + self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); + } else { + self._endTimers[sound._id] = function() { + // Fire ended on this audio node. + self._ended(sound); + + // Clear this listener. + node.removeEventListener('ended', self._endTimers[sound._id], false); + }; + node.addEventListener('ended', self._endTimers[sound._id], false); + } + } catch (err) { + self._emit('playerror', sound._id, err); + } + }; + + // Play immediately if ready, or wait for the 'canplaythrough'e vent. + var loadedNoReadyState = (window && window.ejecta) || (!node.readyState && Howler._navigator.isCocoonJS); + if (node.readyState >= 3 || loadedNoReadyState) { + playHtml5(); + } else { + self._playLock = true; + + var listener = function() { + // Begin playback. + playHtml5(); + + // Clear this listener. + node.removeEventListener(Howler._canPlayEvent, listener, false); + }; + node.addEventListener(Howler._canPlayEvent, listener, false); + + // Cancel the end timer. + self._clearTimer(sound._id); + } + } + + return sound._id; + }, + + /** + * Pause playback and save current position. + * @param {Number} id The sound ID (empty to pause all in group). + * @return {Howl} + */ + pause: function(id) { + var self = this; + + // If the sound hasn't loaded or a play() promise is pending, add it to the load queue to pause when capable. + if (self._state !== 'loaded' || self._playLock) { + self._queue.push({ + event: 'pause', + action: function() { + self.pause(id); + } + }); + + return self; + } + + // If no id is passed, get all ID's to be paused. + var ids = self._getSoundIds(id); + + for (var i=0; i Returns the group's volume value. + * volume(id) -> Returns the sound id's current volume. + * volume(vol) -> Sets the volume of all sounds in this Howl group. + * volume(vol, id) -> Sets the volume of passed sound id. + * @return {Howl/Number} Returns self or current volume. + */ + volume: function() { + var self = this; + var args = arguments; + var vol, id; + + // Determine the values based on arguments. + if (args.length === 0) { + // Return the value of the groups' volume. + return self._volume; + } else if (args.length === 1 || args.length === 2 && typeof args[1] === 'undefined') { + // First check if this is an ID, and if not, assume it is a new volume. + var ids = self._getSoundIds(); + var index = ids.indexOf(args[0]); + if (index >= 0) { + id = parseInt(args[0], 10); + } else { + vol = parseFloat(args[0]); + } + } else if (args.length >= 2) { + vol = parseFloat(args[0]); + id = parseInt(args[1], 10); + } + + // Update the volume or return the current volume. + var sound; + if (typeof vol !== 'undefined' && vol >= 0 && vol <= 1) { + // If the sound hasn't loaded, add it to the load queue to change volume when capable. + if (self._state !== 'loaded'|| self._playLock) { + self._queue.push({ + event: 'volume', + action: function() { + self.volume.apply(self, args); + } + }); + + return self; + } + + // Set the group volume. + if (typeof id === 'undefined') { + self._volume = vol; + } + + // Update one or all volumes. + id = self._getSoundIds(id); + for (var i=0; i 0) ? len / steps : len); + var lastTick = Date.now(); + + // Store the value being faded to. + sound._fadeTo = to; + + // Update the volume value on each interval tick. + sound._interval = setInterval(function() { + // Update the volume based on the time since the last tick. + var tick = (Date.now() - lastTick) / len; + lastTick = Date.now(); + vol += diff * tick; + + // Make sure the volume is in the right bounds. + vol = Math.max(0, vol); + vol = Math.min(1, vol); + + // Round to within 2 decimal points. + vol = Math.round(vol * 100) / 100; + + // Change the volume. + if (self._webAudio) { + sound._volume = vol; + } else { + self.volume(vol, sound._id, true); + } + + // Set the group's volume. + if (isGroup) { + self._volume = vol; + } + + // When the fade is complete, stop it and fire event. + if ((to < from && vol <= to) || (to > from && vol >= to)) { + clearInterval(sound._interval); + sound._interval = null; + sound._fadeTo = null; + self.volume(to, sound._id); + self._emit('fade', sound._id); + } + }, stepLen); + }, + + /** + * Internal method that stops the currently playing fade when + * a new fade starts, volume is changed or the sound is stopped. + * @param {Number} id The sound id. + * @return {Howl} + */ + _stopFade: function(id) { + var self = this; + var sound = self._soundById(id); + + if (sound && sound._interval) { + if (self._webAudio) { + sound._node.gain.cancelScheduledValues(Howler.ctx.currentTime); + } + + clearInterval(sound._interval); + sound._interval = null; + self.volume(sound._fadeTo, id); + sound._fadeTo = null; + self._emit('fade', id); + } + + return self; + }, + + /** + * Get/set the loop parameter on a sound. This method can optionally take 0, 1 or 2 arguments. + * loop() -> Returns the group's loop value. + * loop(id) -> Returns the sound id's loop value. + * loop(loop) -> Sets the loop value for all sounds in this Howl group. + * loop(loop, id) -> Sets the loop value of passed sound id. + * @return {Howl/Boolean} Returns self or current loop value. + */ + loop: function() { + var self = this; + var args = arguments; + var loop, id, sound; + + // Determine the values for loop and id. + if (args.length === 0) { + // Return the grou's loop value. + return self._loop; + } else if (args.length === 1) { + if (typeof args[0] === 'boolean') { + loop = args[0]; + self._loop = loop; + } else { + // Return this sound's loop value. + sound = self._soundById(parseInt(args[0], 10)); + return sound ? sound._loop : false; + } + } else if (args.length === 2) { + loop = args[0]; + id = parseInt(args[1], 10); + } + + // If no id is passed, get all ID's to be looped. + var ids = self._getSoundIds(id); + for (var i=0; i Returns the first sound node's current playback rate. + * rate(id) -> Returns the sound id's current playback rate. + * rate(rate) -> Sets the playback rate of all sounds in this Howl group. + * rate(rate, id) -> Sets the playback rate of passed sound id. + * @return {Howl/Number} Returns self or the current playback rate. + */ + rate: function() { + var self = this; + var args = arguments; + var rate, id; + + // Determine the values based on arguments. + if (args.length === 0) { + // We will simply return the current rate of the first node. + id = self._sounds[0]._id; + } else if (args.length === 1) { + // First check if this is an ID, and if not, assume it is a new rate value. + var ids = self._getSoundIds(); + var index = ids.indexOf(args[0]); + if (index >= 0) { + id = parseInt(args[0], 10); + } else { + rate = parseFloat(args[0]); + } + } else if (args.length === 2) { + rate = parseFloat(args[0]); + id = parseInt(args[1], 10); + } + + // Update the playback rate or return the current value. + var sound; + if (typeof rate === 'number') { + // If the sound hasn't loaded, add it to the load queue to change playback rate when capable. + if (self._state !== 'loaded' || self._playLock) { + self._queue.push({ + event: 'rate', + action: function() { + self.rate.apply(self, args); + } + }); + + return self; + } + + // Set the group rate. + if (typeof id === 'undefined') { + self._rate = rate; + } + + // Update one or all volumes. + id = self._getSoundIds(id); + for (var i=0; i Returns the first sound node's current seek position. + * seek(id) -> Returns the sound id's current seek position. + * seek(seek) -> Sets the seek position of the first sound node. + * seek(seek, id) -> Sets the seek position of passed sound id. + * @return {Howl/Number} Returns self or the current seek position. + */ + seek: function() { + var self = this; + var args = arguments; + var seek, id; + + // Determine the values based on arguments. + if (args.length === 0) { + // We will simply return the current position of the first node. + id = self._sounds[0]._id; + } else if (args.length === 1) { + // First check if this is an ID, and if not, assume it is a new seek position. + var ids = self._getSoundIds(); + var index = ids.indexOf(args[0]); + if (index >= 0) { + id = parseInt(args[0], 10); + } else if (self._sounds.length) { + id = self._sounds[0]._id; + seek = parseFloat(args[0]); + } + } else if (args.length === 2) { + seek = parseFloat(args[0]); + id = parseInt(args[1], 10); + } + + // If there is no ID, bail out. + if (typeof id === 'undefined') { + return self; + } + + // If the sound hasn't loaded, add it to the load queue to seek when capable. + if (self._state !== 'loaded' || self._playLock) { + self._queue.push({ + event: 'seek', + action: function() { + self.seek.apply(self, args); + } + }); + + return self; + } + + // Get the sound. + var sound = self._soundById(id); + + if (sound) { + if (typeof seek === 'number' && seek >= 0) { + // Pause the sound and update position for restarting playback. + var playing = self.playing(id); + if (playing) { + self.pause(id, true); + } + + // Move the position of the track and cancel timer. + sound._seek = seek; + sound._ended = false; + self._clearTimer(id); + + // Update the seek position for HTML5 Audio. + if (!self._webAudio && sound._node && !isNaN(sound._node.duration)) { + sound._node.currentTime = seek; + } + + // Seek and emit when ready. + var seekAndEmit = function() { + self._emit('seek', id); + + // Restart the playback if the sound was playing. + if (playing) { + self.play(id, true); + } + }; + + // Wait for the play lock to be unset before emitting (HTML5 Audio). + if (playing && !self._webAudio) { + var emitSeek = function() { + if (!self._playLock) { + seekAndEmit(); + } else { + setTimeout(emitSeek, 0); + } + }; + setTimeout(emitSeek, 0); + } else { + seekAndEmit(); + } + } else { + if (self._webAudio) { + var realTime = self.playing(id) ? Howler.ctx.currentTime - sound._playStart : 0; + var rateSeek = sound._rateSeek ? sound._rateSeek - sound._seek : 0; + return sound._seek + (rateSeek + realTime * Math.abs(sound._rate)); + } else { + return sound._node.currentTime; + } + } + } + + return self; + }, + + /** + * Check if a specific sound is currently playing or not (if id is provided), or check if at least one of the sounds in the group is playing or not. + * @param {Number} id The sound id to check. If none is passed, the whole sound group is checked. + * @return {Boolean} True if playing and false if not. + */ + playing: function(id) { + var self = this; + + // Check the passed sound ID (if any). + if (typeof id === 'number') { + var sound = self._soundById(id); + return sound ? !sound._paused : false; + } + + // Otherwise, loop through all sounds and check if any are playing. + for (var i=0; i= 0) { + Howler._howls.splice(index, 1); + } + + // Delete this sound from the cache (if no other Howl is using it). + var remCache = true; + for (i=0; i= 0) { + remCache = false; + break; + } + } + + if (cache && remCache) { + delete cache[self._src]; + } + + // Clear global errors. + Howler.noAudio = false; + + // Clear out `self`. + self._state = 'unloaded'; + self._sounds = []; + self = null; + + return null; + }, + + /** + * Listen to a custom event. + * @param {String} event Event name. + * @param {Function} fn Listener to call. + * @param {Number} id (optional) Only listen to events for this sound. + * @param {Number} once (INTERNAL) Marks event to fire only once. + * @return {Howl} + */ + on: function(event, fn, id, once) { + var self = this; + var events = self['_on' + event]; + + if (typeof fn === 'function') { + events.push(once ? {id: id, fn: fn, once: once} : {id: id, fn: fn}); + } + + return self; + }, + + /** + * Remove a custom event. Call without parameters to remove all events. + * @param {String} event Event name. + * @param {Function} fn Listener to remove. Leave empty to remove all. + * @param {Number} id (optional) Only remove events for this sound. + * @return {Howl} + */ + off: function(event, fn, id) { + var self = this; + var events = self['_on' + event]; + var i = 0; + + // Allow passing just an event and ID. + if (typeof fn === 'number') { + id = fn; + fn = null; + } + + if (fn || id) { + // Loop through event store and remove the passed function. + for (i=0; i=0; i--) { + // Only fire the listener if the correct ID is used. + if (!events[i].id || events[i].id === id || event === 'load') { + setTimeout(function(fn) { + fn.call(this, id, msg); + }.bind(self, events[i].fn), 0); + + // If this event was setup with `once`, remove it. + if (events[i].once) { + self.off(event, events[i].fn, events[i].id); + } + } + } + + // Pass the event type into load queue so that it can continue stepping. + self._loadQueue(event); + + return self; + }, + + /** + * Queue of actions initiated before the sound has loaded. + * These will be called in sequence, with the next only firing + * after the previous has finished executing (even if async like play). + * @return {Howl} + */ + _loadQueue: function(event) { + var self = this; + + if (self._queue.length > 0) { + var task = self._queue[0]; + + // Remove this task if a matching event was passed. + if (task.event === event) { + self._queue.shift(); + self._loadQueue(); + } + + // Run the task if no event type is passed. + if (!event) { + task.action(); + } + } + + return self; + }, + + /** + * Fired when playback ends at the end of the duration. + * @param {Sound} sound The sound object to work with. + * @return {Howl} + */ + _ended: function(sound) { + var self = this; + var sprite = sound._sprite; + + // If we are using IE and there was network latency we may be clipping + // audio before it completes playing. Lets check the node to make sure it + // believes it has completed, before ending the playback. + if (!self._webAudio && sound._node && !sound._node.paused && !sound._node.ended && sound._node.currentTime < sound._stop) { + setTimeout(self._ended.bind(self, sound), 100); + return self; + } + + // Should this sound loop? + var loop = !!(sound._loop || self._sprite[sprite][2]); + + // Fire the ended event. + self._emit('end', sound._id); + + // Restart the playback for HTML5 Audio loop. + if (!self._webAudio && loop) { + self.stop(sound._id, true).play(sound._id); + } + + // Restart this timer if on a Web Audio loop. + if (self._webAudio && loop) { + self._emit('play', sound._id); + sound._seek = sound._start || 0; + sound._rateSeek = 0; + sound._playStart = Howler.ctx.currentTime; + + var timeout = ((sound._stop - sound._start) * 1000) / Math.abs(sound._rate); + self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); + } + + // Mark the node as paused. + if (self._webAudio && !loop) { + sound._paused = true; + sound._ended = true; + sound._seek = sound._start || 0; + sound._rateSeek = 0; + self._clearTimer(sound._id); + + // Clean up the buffer source. + self._cleanBuffer(sound._node); + + // Attempt to auto-suspend AudioContext if no sounds are still playing. + Howler._autoSuspend(); + } + + // When using a sprite, end the track. + if (!self._webAudio && !loop) { + self.stop(sound._id, true); + } + + return self; + }, + + /** + * Clear the end timer for a sound playback. + * @param {Number} id The sound ID. + * @return {Howl} + */ + _clearTimer: function(id) { + var self = this; + + if (self._endTimers[id]) { + // Clear the timeout or remove the ended listener. + if (typeof self._endTimers[id] !== 'function') { + clearTimeout(self._endTimers[id]); + } else { + var sound = self._soundById(id); + if (sound && sound._node) { + sound._node.removeEventListener('ended', self._endTimers[id], false); + } + } + + delete self._endTimers[id]; + } + + return self; + }, + + /** + * Return the sound identified by this ID, or return null. + * @param {Number} id Sound ID + * @return {Object} Sound object or null. + */ + _soundById: function(id) { + var self = this; + + // Loop through all sounds and find the one with this ID. + for (var i=0; i=0; i--) { + if (cnt <= limit) { + return; + } + + if (self._sounds[i]._ended) { + // Disconnect the audio source when using Web Audio. + if (self._webAudio && self._sounds[i]._node) { + self._sounds[i]._node.disconnect(0); + } + + // Remove sounds until we have the pool size. + self._sounds.splice(i, 1); + cnt--; + } + } + }, + + /** + * Get all ID's from the sounds pool. + * @param {Number} id Only return one ID if one is passed. + * @return {Array} Array of IDs. + */ + _getSoundIds: function(id) { + var self = this; + + if (typeof id === 'undefined') { + var ids = []; + for (var i=0; i= 0; + + if (Howler._scratchBuffer && node.bufferSource) { + node.bufferSource.onended = null; + node.bufferSource.disconnect(0); + if (isIOS) { + try { node.bufferSource.buffer = Howler._scratchBuffer; } catch(e) {} + } + } + node.bufferSource = null; + + return self; + } + }; + + /** Single Sound Methods **/ + /***************************************************************************/ + + /** + * Setup the sound object, which each node attached to a Howl group is contained in. + * @param {Object} howl The Howl parent group. + */ + var Sound = function(howl) { + this._parent = howl; + this.init(); + }; + Sound.prototype = { + /** + * Initialize a new Sound object. + * @return {Sound} + */ + init: function() { + var self = this; + var parent = self._parent; + + // Setup the default parameters. + self._muted = parent._muted; + self._loop = parent._loop; + self._volume = parent._volume; + self._rate = parent._rate; + self._seek = 0; + self._paused = true; + self._ended = true; + self._sprite = '__default'; + + // Generate a unique ID for this sound. + self._id = ++Howler._counter; + + // Add itself to the parent's pool. + parent._sounds.push(self); + + // Create the new node. + self.create(); + + return self; + }, + + /** + * Create and setup a new sound object, whether HTML5 Audio or Web Audio. + * @return {Sound} + */ + create: function() { + var self = this; + var parent = self._parent; + var volume = (Howler._muted || self._muted || self._parent._muted) ? 0 : self._volume; + + if (parent._webAudio) { + // Create the gain node for controlling volume (the source will connect to this). + self._node = (typeof Howler.ctx.createGain === 'undefined') ? Howler.ctx.createGainNode() : Howler.ctx.createGain(); + self._node.gain.setValueAtTime(volume, Howler.ctx.currentTime); + self._node.paused = true; + self._node.connect(Howler.masterGain); + } else { + // Get an unlocked Audio object from the pool. + self._node = Howler._obtainHtml5Audio(); + + // Listen for errors (http://dev.w3.org/html5/spec-author-view/spec.html#mediaerror). + self._errorFn = self._errorListener.bind(self); + self._node.addEventListener('error', self._errorFn, false); + + // Listen for 'canplaythrough' event to let us know the sound is ready. + self._loadFn = self._loadListener.bind(self); + self._node.addEventListener(Howler._canPlayEvent, self._loadFn, false); + + // Setup the new audio node. + self._node.src = parent._src; + self._node.preload = 'auto'; + self._node.volume = volume * Howler.volume(); + + // Begin loading the source. + self._node.load(); + } + + return self; + }, + + /** + * Reset the parameters of this sound to the original state (for recycle). + * @return {Sound} + */ + reset: function() { + var self = this; + var parent = self._parent; + + // Reset all of the parameters of this sound. + self._muted = parent._muted; + self._loop = parent._loop; + self._volume = parent._volume; + self._rate = parent._rate; + self._seek = 0; + self._rateSeek = 0; + self._paused = true; + self._ended = true; + self._sprite = '__default'; + + // Generate a new ID so that it isn't confused with the previous sound. + self._id = ++Howler._counter; + + return self; + }, + + /** + * HTML5 Audio error listener callback. + */ + _errorListener: function() { + var self = this; + + // Fire an error event and pass back the code. + self._parent._emit('loaderror', self._id, self._node.error ? self._node.error.code : 0); + + // Clear the event listener. + self._node.removeEventListener('error', self._errorFn, false); + }, + + /** + * HTML5 Audio canplaythrough listener callback. + */ + _loadListener: function() { + var self = this; + var parent = self._parent; + + // Round up the duration to account for the lower precision in HTML5 Audio. + parent._duration = Math.ceil(self._node.duration * 10) / 10; + + // Setup a sprite if none is defined. + if (Object.keys(parent._sprite).length === 0) { + parent._sprite = {__default: [0, parent._duration * 1000]}; + } + + if (parent._state !== 'loaded') { + parent._state = 'loaded'; + parent._emit('load'); + parent._loadQueue(); + } + + // Clear the event listener. + self._node.removeEventListener(Howler._canPlayEvent, self._loadFn, false); + } + }; + + /** Helper Methods **/ + /***************************************************************************/ + + var cache = {}; + + /** + * Buffer a sound from URL, Data URI or cache and decode to audio source (Web Audio API). + * @param {Howl} self + */ + var loadBuffer = function(self) { + var url = self._src; + + // Check if the buffer has already been cached and use it instead. + if (cache[url]) { + // Set the duration from the cache. + self._duration = cache[url].duration; + + // Load the sound into this Howl. + loadSound(self); + + return; + } + + if (/^data:[^;]+;base64,/.test(url)) { + // Decode the base64 data URI without XHR, since some browsers don't support it. + var data = atob(url.split(',')[1]); + var dataView = new Uint8Array(data.length); + for (var i=0; i 0) { + cache[self._src] = buffer; + loadSound(self, buffer); + } else { + error(); + } + }; + + // Decode the buffer into an audio source. + if (typeof Promise !== 'undefined' && Howler.ctx.decodeAudioData.length === 1) { + Howler.ctx.decodeAudioData(arraybuffer).then(success).catch(error); + } else { + Howler.ctx.decodeAudioData(arraybuffer, success, error); + } + }; + + /** + * Sound is now loaded, so finish setting everything up and fire the loaded event. + * @param {Howl} self + * @param {Object} buffer The decoded buffer sound source. + */ + var loadSound = function(self, buffer) { + // Set the duration. + if (buffer && !self._duration) { + self._duration = buffer.duration; + } + + // Setup a sprite if none is defined. + if (Object.keys(self._sprite).length === 0) { + self._sprite = {__default: [0, self._duration * 1000]}; + } + + // Fire the loaded event. + if (self._state !== 'loaded') { + self._state = 'loaded'; + self._emit('load'); + self._loadQueue(); + } + }; + + /** + * Setup the audio context when available, or switch to HTML5 Audio mode. + */ + var setupAudioContext = function() { + // If we have already detected that Web Audio isn't supported, don't run this step again. + if (!Howler.usingWebAudio) { + return; + } + + // Check if we are using Web Audio and setup the AudioContext if we are. + try { + if (typeof AudioContext !== 'undefined') { + Howler.ctx = new AudioContext(); + } else if (typeof webkitAudioContext !== 'undefined') { + Howler.ctx = new webkitAudioContext(); + } else { + Howler.usingWebAudio = false; + } + } catch(e) { + Howler.usingWebAudio = false; + } + + // If the audio context creation still failed, set using web audio to false. + if (!Howler.ctx) { + Howler.usingWebAudio = false; + } + + // Check if a webview is being used on iOS8 or earlier (rather than the browser). + // If it is, disable Web Audio as it causes crashing. + var iOS = (/iP(hone|od|ad)/.test(Howler._navigator && Howler._navigator.platform)); + var appVersion = Howler._navigator && Howler._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/); + var version = appVersion ? parseInt(appVersion[1], 10) : null; + if (iOS && version && version < 9) { + var safari = /safari/.test(Howler._navigator && Howler._navigator.userAgent.toLowerCase()); + if (Howler._navigator && Howler._navigator.standalone && !safari || Howler._navigator && !Howler._navigator.standalone && !safari) { + Howler.usingWebAudio = false; + } + } + + // Create and expose the master GainNode when using Web Audio (useful for plugins or advanced usage). + if (Howler.usingWebAudio) { + Howler.masterGain = (typeof Howler.ctx.createGain === 'undefined') ? Howler.ctx.createGainNode() : Howler.ctx.createGain(); + Howler.masterGain.gain.setValueAtTime(Howler._muted ? 0 : 1, Howler.ctx.currentTime); + Howler.masterGain.connect(Howler.ctx.destination); + } + + // Re-run the setup on Howler. + Howler._setup(); + }; + + // Add support for CommonJS libraries such as browserify. + { + exports.Howler = Howler; + exports.Howl = Howl; + } + + // Define globally in case AMD is not available or unused. + if (typeof window !== 'undefined') { + window.HowlerGlobal = HowlerGlobal; + window.Howler = Howler; + window.Howl = Howl; + window.Sound = Sound; + } else if (typeof commonjsGlobal !== 'undefined') { // Add to global in Node.js (for testing, etc). + commonjsGlobal.HowlerGlobal = HowlerGlobal; + commonjsGlobal.Howler = Howler; + commonjsGlobal.Howl = Howl; + commonjsGlobal.Sound = Sound; + } + })(); + + + /*! + * Spatial Plugin - Adds support for stereo and 3D audio where Web Audio is supported. + * + * howler.js v2.1.1 + * howlerjs.com + * + * (c) 2013-2018, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ + + (function() { + + // Setup default properties. + HowlerGlobal.prototype._pos = [0, 0, 0]; + HowlerGlobal.prototype._orientation = [0, 0, -1, 0, 1, 0]; + + /** Global Methods **/ + /***************************************************************************/ + + /** + * Helper method to update the stereo panning position of all current Howls. + * Future Howls will not use this value unless explicitly set. + * @param {Number} pan A value of -1.0 is all the way left and 1.0 is all the way right. + * @return {Howler/Number} Self or current stereo panning value. + */ + HowlerGlobal.prototype.stereo = function(pan) { + var self = this; + + // Stop right here if not using Web Audio. + if (!self.ctx || !self.ctx.listener) { + return self; + } + + // Loop through all Howls and update their stereo panning. + for (var i=self._howls.length-1; i>=0; i--) { + self._howls[i].stereo(pan); + } + + return self; + }; + + /** + * Get/set the position of the listener in 3D cartesian space. Sounds using + * 3D position will be relative to the listener's position. + * @param {Number} x The x-position of the listener. + * @param {Number} y The y-position of the listener. + * @param {Number} z The z-position of the listener. + * @return {Howler/Array} Self or current listener position. + */ + HowlerGlobal.prototype.pos = function(x, y, z) { + var self = this; + + // Stop right here if not using Web Audio. + if (!self.ctx || !self.ctx.listener) { + return self; + } + + // Set the defaults for optional 'y' & 'z'. + y = (typeof y !== 'number') ? self._pos[1] : y; + z = (typeof z !== 'number') ? self._pos[2] : z; + + if (typeof x === 'number') { + self._pos = [x, y, z]; + + if (typeof self.ctx.listener.positionX !== 'undefined') { + self.ctx.listener.positionX.setTargetAtTime(self._pos[0], Howler.ctx.currentTime, 0.1); + self.ctx.listener.positionY.setTargetAtTime(self._pos[1], Howler.ctx.currentTime, 0.1); + self.ctx.listener.positionZ.setTargetAtTime(self._pos[2], Howler.ctx.currentTime, 0.1); + } else { + self.ctx.listener.setPosition(self._pos[0], self._pos[1], self._pos[2]); + } + } else { + return self._pos; + } + + return self; + }; + + /** + * Get/set the direction the listener is pointing in the 3D cartesian space. + * A front and up vector must be provided. The front is the direction the + * face of the listener is pointing, and up is the direction the top of the + * listener is pointing. Thus, these values are expected to be at right angles + * from each other. + * @param {Number} x The x-orientation of the listener. + * @param {Number} y The y-orientation of the listener. + * @param {Number} z The z-orientation of the listener. + * @param {Number} xUp The x-orientation of the top of the listener. + * @param {Number} yUp The y-orientation of the top of the listener. + * @param {Number} zUp The z-orientation of the top of the listener. + * @return {Howler/Array} Returns self or the current orientation vectors. + */ + HowlerGlobal.prototype.orientation = function(x, y, z, xUp, yUp, zUp) { + var self = this; + + // Stop right here if not using Web Audio. + if (!self.ctx || !self.ctx.listener) { + return self; + } + + // Set the defaults for optional 'y' & 'z'. + var or = self._orientation; + y = (typeof y !== 'number') ? or[1] : y; + z = (typeof z !== 'number') ? or[2] : z; + xUp = (typeof xUp !== 'number') ? or[3] : xUp; + yUp = (typeof yUp !== 'number') ? or[4] : yUp; + zUp = (typeof zUp !== 'number') ? or[5] : zUp; + + if (typeof x === 'number') { + self._orientation = [x, y, z, xUp, yUp, zUp]; + + if (typeof self.ctx.listener.forwardX !== 'undefined') { + self.ctx.listener.forwardX.setTargetAtTime(x, Howler.ctx.currentTime, 0.1); + self.ctx.listener.forwardY.setTargetAtTime(y, Howler.ctx.currentTime, 0.1); + self.ctx.listener.forwardZ.setTargetAtTime(z, Howler.ctx.currentTime, 0.1); + self.ctx.listener.upX.setTargetAtTime(x, Howler.ctx.currentTime, 0.1); + self.ctx.listener.upY.setTargetAtTime(y, Howler.ctx.currentTime, 0.1); + self.ctx.listener.upZ.setTargetAtTime(z, Howler.ctx.currentTime, 0.1); + } else { + self.ctx.listener.setOrientation(x, y, z, xUp, yUp, zUp); + } + } else { + return or; + } + + return self; + }; + + /** Group Methods **/ + /***************************************************************************/ + + /** + * Add new properties to the core init. + * @param {Function} _super Core init method. + * @return {Howl} + */ + Howl.prototype.init = (function(_super) { + return function(o) { + var self = this; + + // Setup user-defined default properties. + self._orientation = o.orientation || [1, 0, 0]; + self._stereo = o.stereo || null; + self._pos = o.pos || null; + self._pannerAttr = { + coneInnerAngle: typeof o.coneInnerAngle !== 'undefined' ? o.coneInnerAngle : 360, + coneOuterAngle: typeof o.coneOuterAngle !== 'undefined' ? o.coneOuterAngle : 360, + coneOuterGain: typeof o.coneOuterGain !== 'undefined' ? o.coneOuterGain : 0, + distanceModel: typeof o.distanceModel !== 'undefined' ? o.distanceModel : 'inverse', + maxDistance: typeof o.maxDistance !== 'undefined' ? o.maxDistance : 10000, + panningModel: typeof o.panningModel !== 'undefined' ? o.panningModel : 'HRTF', + refDistance: typeof o.refDistance !== 'undefined' ? o.refDistance : 1, + rolloffFactor: typeof o.rolloffFactor !== 'undefined' ? o.rolloffFactor : 1 + }; + + // Setup event listeners. + self._onstereo = o.onstereo ? [{fn: o.onstereo}] : []; + self._onpos = o.onpos ? [{fn: o.onpos}] : []; + self._onorientation = o.onorientation ? [{fn: o.onorientation}] : []; + + // Complete initilization with howler.js core's init function. + return _super.call(this, o); + }; + })(Howl.prototype.init); + + /** + * Get/set the stereo panning of the audio source for this sound or all in the group. + * @param {Number} pan A value of -1.0 is all the way left and 1.0 is all the way right. + * @param {Number} id (optional) The sound ID. If none is passed, all in group will be updated. + * @return {Howl/Number} Returns self or the current stereo panning value. + */ + Howl.prototype.stereo = function(pan, id) { + var self = this; + + // Stop right here if not using Web Audio. + if (!self._webAudio) { + return self; + } + + // If the sound hasn't loaded, add it to the load queue to change stereo pan when capable. + if (self._state !== 'loaded') { + self._queue.push({ + event: 'stereo', + action: function() { + self.stereo(pan, id); + } + }); + + return self; + } + + // Check for PannerStereoNode support and fallback to PannerNode if it doesn't exist. + var pannerType = (typeof Howler.ctx.createStereoPanner === 'undefined') ? 'spatial' : 'stereo'; + + // Setup the group's stereo panning if no ID is passed. + if (typeof id === 'undefined') { + // Return the group's stereo panning if no parameters are passed. + if (typeof pan === 'number') { + self._stereo = pan; + self._pos = [pan, 0, 0]; + } else { + return self._stereo; + } + } + + // Change the streo panning of one or all sounds in group. + var ids = self._getSoundIds(id); + for (var i=0; i Returns the group's values. + * pannerAttr(id) -> Returns the sound id's values. + * pannerAttr(o) -> Set's the values of all sounds in this Howl group. + * pannerAttr(o, id) -> Set's the values of passed sound id. + * + * Attributes: + * coneInnerAngle - (360 by default) A parameter for directional audio sources, this is an angle, in degrees, + * inside of which there will be no volume reduction. + * coneOuterAngle - (360 by default) A parameter for directional audio sources, this is an angle, in degrees, + * outside of which the volume will be reduced to a constant value of `coneOuterGain`. + * coneOuterGain - (0 by default) A parameter for directional audio sources, this is the gain outside of the + * `coneOuterAngle`. It is a linear value in the range `[0, 1]`. + * distanceModel - ('inverse' by default) Determines algorithm used to reduce volume as audio moves away from + * listener. Can be `linear`, `inverse` or `exponential. + * maxDistance - (10000 by default) The maximum distance between source and listener, after which the volume + * will not be reduced any further. + * refDistance - (1 by default) A reference distance for reducing volume as source moves further from the listener. + * This is simply a variable of the distance model and has a different effect depending on which model + * is used and the scale of your coordinates. Generally, volume will be equal to 1 at this distance. + * rolloffFactor - (1 by default) How quickly the volume reduces as source moves from listener. This is simply a + * variable of the distance model and can be in the range of `[0, 1]` with `linear` and `[0, ∞]` + * with `inverse` and `exponential`. + * panningModel - ('HRTF' by default) Determines which spatialization algorithm is used to position audio. + * Can be `HRTF` or `equalpower`. + * + * @return {Howl/Object} Returns self or current panner attributes. + */ + Howl.prototype.pannerAttr = function() { + var self = this; + var args = arguments; + var o, id, sound; + + // Stop right here if not using Web Audio. + if (!self._webAudio) { + return self; + } + + // Determine the values based on arguments. + if (args.length === 0) { + // Return the group's panner attribute values. + return self._pannerAttr; + } else if (args.length === 1) { + if (typeof args[0] === 'object') { + o = args[0]; + + // Set the grou's panner attribute values. + if (typeof id === 'undefined') { + if (!o.pannerAttr) { + o.pannerAttr = { + coneInnerAngle: o.coneInnerAngle, + coneOuterAngle: o.coneOuterAngle, + coneOuterGain: o.coneOuterGain, + distanceModel: o.distanceModel, + maxDistance: o.maxDistance, + refDistance: o.refDistance, + rolloffFactor: o.rolloffFactor, + panningModel: o.panningModel + }; + } + + self._pannerAttr = { + coneInnerAngle: typeof o.pannerAttr.coneInnerAngle !== 'undefined' ? o.pannerAttr.coneInnerAngle : self._coneInnerAngle, + coneOuterAngle: typeof o.pannerAttr.coneOuterAngle !== 'undefined' ? o.pannerAttr.coneOuterAngle : self._coneOuterAngle, + coneOuterGain: typeof o.pannerAttr.coneOuterGain !== 'undefined' ? o.pannerAttr.coneOuterGain : self._coneOuterGain, + distanceModel: typeof o.pannerAttr.distanceModel !== 'undefined' ? o.pannerAttr.distanceModel : self._distanceModel, + maxDistance: typeof o.pannerAttr.maxDistance !== 'undefined' ? o.pannerAttr.maxDistance : self._maxDistance, + refDistance: typeof o.pannerAttr.refDistance !== 'undefined' ? o.pannerAttr.refDistance : self._refDistance, + rolloffFactor: typeof o.pannerAttr.rolloffFactor !== 'undefined' ? o.pannerAttr.rolloffFactor : self._rolloffFactor, + panningModel: typeof o.pannerAttr.panningModel !== 'undefined' ? o.pannerAttr.panningModel : self._panningModel + }; + } + } else { + // Return this sound's panner attribute values. + sound = self._soundById(parseInt(args[0], 10)); + return sound ? sound._pannerAttr : self._pannerAttr; + } + } else if (args.length === 2) { + o = args[0]; + id = parseInt(args[1], 10); + } + + // Update the values of the specified sounds. + var ids = self._getSoundIds(id); + for (var i=0; i 3) { + // something went wrong + var errmsg = "melonJS: failed loading " + sound_name; + + if (me.sys.stopOnAudioError === false) { + // disable audio + me.audio.disable(); // call error callback if defined + + if (onerror_cb) { + onerror_cb(); + } // warning + + + console.log(errmsg + ", disabling audio"); + } else { + // throw an exception and stop everything ! + throw new Error(errmsg); + } // else try loading again ! + + } else { + audioTracks[sound_name].load(); + } + } + /* + * PUBLIC STUFF + */ + + /** + * Initialize and configure the audio support.
+ * 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.} + */ + var T_POINTERS = []; // list of registered Event handlers + + var eventHandlers = new Map(); // a cache rect represeting the current pointer area + + var currentPointer = new me.Rect(0, 0, 1, 1); // some useful flags + + var pointerInitialized = false; // Track last event timestamp to prevent firing events out of order + + var lastTimeStamp = 0; // "active" list of supported events + + var activeEventList = []; // internal constants + + var WHEEL = ["wheel"]; + var POINTER_MOVE = ["pointermove", "mousemove", "touchmove"]; + var POINTER_DOWN = ["pointerdown", "mousedown", "touchstart"]; + var POINTER_UP = ["pointerup", "mouseup", "touchend"]; + var POINTER_CANCEL = ["pointercancel", "mousecancel", "touchcancel"]; + var POINTER_ENTER = ["pointerenter", "mouseenter", "touchenter"]; + var POINTER_OVER = ["pointerover", "mouseover", "touchover"]; + var POINTER_LEAVE = ["pointerleave", "mouseleave", "touchleave"]; // list of standard pointer event type + + var pointerEventList = [WHEEL[0], POINTER_MOVE[0], POINTER_DOWN[0], POINTER_UP[0], POINTER_CANCEL[0], POINTER_ENTER[0], POINTER_OVER[0], POINTER_LEAVE[0]]; // legacy mouse event type + + var mouseEventList = [WHEEL[0], POINTER_MOVE[1], POINTER_DOWN[1], POINTER_UP[1], POINTER_CANCEL[1], POINTER_ENTER[1], POINTER_OVER[1], POINTER_LEAVE[1]]; // iOS style touch event type + + var touchEventList = [POINTER_MOVE[2], POINTER_DOWN[2], POINTER_UP[2], POINTER_CANCEL[2], POINTER_ENTER[2], POINTER_OVER[2], POINTER_LEAVE[2]]; + var pointerEventMap = { + wheel: WHEEL, + pointermove: POINTER_MOVE, + pointerdown: POINTER_DOWN, + pointerup: POINTER_UP, + pointercancel: POINTER_CANCEL, + pointerenter: POINTER_ENTER, + pointerover: POINTER_OVER, + pointerleave: POINTER_LEAVE + }; + /** + * Array of normalized events (mouse, touch, pointer) + * @ignore + */ + + var normalizedEvents = []; + /** + * addEventListerner for the specified event list and callback + * @ignore + */ + + function registerEventListener(eventList, callback) { + for (var x = 0; x < eventList.length; x++) { + if (POINTER_MOVE.indexOf(eventList[x]) === -1) { + me.video.renderer.getScreenCanvas().addEventListener(eventList[x], callback, false); + } + } + } + /** + * enable pointer event (Pointer/Mouse/Touch) + * @ignore + */ + + + function enablePointerEvent() { + if (!pointerInitialized) { + // instantiate a pool of pointer catched + for (var v = 0; v < me.device.maxTouchPoints; v++) { + T_POINTERS.push(new me.Pointer()); + } + + if (me.device.PointerEvent) { + // standard Pointer Events + activeEventList = pointerEventList; + } else { + // Regular Mouse events + activeEventList = mouseEventList; + } + + if (me.device.touch && !me.device.PointerEvent) { + // touch event on mobile devices + activeEventList = activeEventList.concat(touchEventList); + } + + registerEventListener(activeEventList, onPointerEvent); // If W3C standard wheel events are not available, use non-standard + + if (!me.device.wheel) { + window.addEventListener("mousewheel", onMouseWheel, false); + } // set the PointerMove/touchMove/MouseMove event + + + if (typeof api.throttlingInterval === "undefined") { + // set the default value + api.throttlingInterval = ~~(1000 / me.sys.fps); + } + + if (me.sys.autoFocus === true) { + me.device.focus(); + me.video.renderer.getScreenCanvas().addEventListener(activeEventList[2], // MOUSE/POINTER DOWN + function () { + me.device.focus(); + }, { + passive: true + }); + } // if time interval <= 16, disable the feature + + + var i; + var events = findAllActiveEvents(activeEventList, POINTER_MOVE); + + if (api.throttlingInterval < 17) { + for (i = 0; i < events.length; i++) { + if (activeEventList.indexOf(events[i]) !== -1) { + me.video.renderer.getScreenCanvas().addEventListener(events[i], onMoveEvent, false); + } + } + } else { + for (i = 0; i < events.length; i++) { + if (activeEventList.indexOf(events[i]) !== -1) { + me.video.renderer.getScreenCanvas().addEventListener(events[i], me.utils.function.throttle(onMoveEvent, api.throttlingInterval, false), false); + } + } + } // disable all gesture by default + + + me.input.setTouchAction(me.video.renderer.getScreenCanvas()); + pointerInitialized = true; + } + } + /** + * @ignore + */ + + + function findActiveEvent(activeEventList, eventTypes) { + for (var i = 0; i < eventTypes.length; i++) { + var event = activeEventList.indexOf(eventTypes[i]); + + if (event !== -1) { + return eventTypes[i]; + } + } + } + /** + * @ignore + */ + + + function findAllActiveEvents(activeEventList, eventTypes) { + var events = []; + + for (var i = 0; i < eventTypes.length; i++) { + var event = activeEventList.indexOf(eventTypes[i]); + + if (event !== -1) { + events.push(eventTypes[i]); + } + } + + return events; + } + /** + * @ignore + */ + + + function triggerEvent(handlers, type, pointer, pointerId) { + var callback; + + if (handlers.callbacks[type]) { + handlers.pointerId = pointerId; + + for (var i = handlers.callbacks[type].length - 1; i >= 0 && (callback = handlers.callbacks[type][i]); i--) { + if (callback(pointer) === false) { + // stop propagating the event if return false + return true; + } + } + } + + return false; + } + /** + * propagate events to registered objects + * @ignore + */ + + + function dispatchEvent(normalizedEvents) { + var handled = false; + + while (normalizedEvents.length > 0) { + // keep a reference to the last item + var pointer = normalizedEvents.pop(); // and put it back into our cache + + T_POINTERS.push(pointer); // Do not fire older events + + if (typeof pointer.event.timeStamp !== "undefined") { + if (pointer.event.timeStamp < lastTimeStamp) { + continue; + } + + lastTimeStamp = pointer.event.timeStamp; + } + + currentPointer.setShape(pointer.gameWorldX, pointer.gameWorldY, pointer.width, pointer.height); // trigger a global event for pointer move + + if (POINTER_MOVE.includes(pointer.type)) { + pointer.gameX = pointer.gameLocalX = pointer.gameScreenX; + pointer.gameY = pointer.gameLocalY = pointer.gameScreenY; + me.event.publish(me.event.POINTERMOVE, [pointer]); + } + + var candidates = me.collision.quadTree.retrieve(currentPointer, me.Container.prototype._sortReverseZ); // add the main viewport to the list of candidates + + candidates = candidates.concat([me.game.viewport]); + + for (var c = candidates.length, candidate; c--, candidate = candidates[c];) { + if (eventHandlers.has(candidate) && candidate.isKinematic !== true) { + var handlers = eventHandlers.get(candidate); + var region = handlers.region; + var ancestor = region.ancestor; + var bounds = region.getBounds(); + var eventInBounds = false; + + if (region.floating === true) { + pointer.gameX = pointer.gameLocalX = pointer.gameScreenX; + pointer.gameY = pointer.gameLocalY = pointer.gameScreenY; + } else { + pointer.gameX = pointer.gameLocalX = pointer.gameWorldX; + pointer.gameY = pointer.gameLocalY = pointer.gameWorldY; + } // adjust gameLocalX to specify coordinates + // within the region ancestor container + + + if (typeof ancestor !== "undefined") { + var parentPos = ancestor.getBounds().pos; + pointer.gameLocalX = pointer.gameX - parentPos.x; + pointer.gameLocalY = pointer.gameY - parentPos.y; + } + + if (region instanceof me.Sprite) { + var gameX = pointer.gameX; + var gameY = pointer.gameY; + + if (!region.currentTransform.isIdentity()) { + var invV = region.currentTransform.multiplyVectorInverse(me.pool.pull("me.Vector2d", gameX, gameY)); + gameX = invV.x; + gameY = invV.y; + me.pool.push(invV); + } + + eventInBounds = bounds.containsPoint(gameX, gameY); + } else { + eventInBounds = bounds.containsPoint(pointer.gameX, pointer.gameY) && (bounds === region || // if the given target is another shape than me.Rect + region.containsPoint(pointer.gameLocalX, pointer.gameLocalY)); + } + + switch (pointer.type) { + case POINTER_MOVE[0]: + case POINTER_MOVE[1]: + case POINTER_MOVE[2]: + case POINTER_MOVE[3]: + // moved out of bounds: trigger the POINTER_LEAVE callbacks + if (handlers.pointerId === pointer.pointerId && !eventInBounds) { + if (triggerEvent(handlers, findActiveEvent(activeEventList, POINTER_LEAVE), pointer, null)) { + handled = true; + break; + } + } // no pointer & moved inside of bounds: trigger the POINTER_ENTER callbacks + else if (handlers.pointerId === null && eventInBounds) { + if (triggerEvent(handlers, findActiveEvent(activeEventList, POINTER_ENTER), pointer, pointer.pointerId)) { + handled = true; + break; + } + } // trigger the POINTER_MOVE callbacks + + + if (eventInBounds && triggerEvent(handlers, pointer.type, pointer, pointer.pointerId)) { + handled = true; + break; + } + + break; + + case POINTER_UP[0]: + case POINTER_UP[1]: + case POINTER_UP[2]: + case POINTER_UP[3]: + // pointer defined & inside of bounds: trigger the POINTER_UP callback + if (handlers.pointerId === pointer.pointerId && eventInBounds) { + // trigger the corresponding callback + if (triggerEvent(handlers, pointer.type, pointer, null)) { + handled = true; + break; + } + } + + break; + + case POINTER_CANCEL[0]: + case POINTER_CANCEL[1]: + case POINTER_CANCEL[2]: + case POINTER_CANCEL[3]: + // pointer defined: trigger the POINTER_CANCEL callback + if (handlers.pointerId === pointer.pointerId) { + // trigger the corresponding callback + if (triggerEvent(handlers, pointer.type, pointer, null)) { + handled = true; + break; + } + } + + break; + + default: + // event inside of bounds: trigger the POINTER_DOWN or WHEEL callback + if (eventInBounds) { + // trigger the corresponding callback + if (triggerEvent(handlers, pointer.type, pointer, pointer.pointerId)) { + handled = true; + + if (pointer.type === "wheel") { + api._preventDefaultFn(pointer.event); + } + + break; + } + } + + break; + } + } + + if (handled === true) { + // stop iterating through this list of candidates + break; + } + } + } + + return handled; + } + /** + * translate event coordinates + * @ignore + */ + + + function normalizeEvent(event) { + var pointer; // PointerEvent or standard Mouse event + + if (me.device.TouchEvent && event.changedTouches) { + // iOS/Android Touch event + for (var i = 0, l = event.changedTouches.length; i < l; i++) { + var touchEvent = event.changedTouches[i]; + pointer = T_POINTERS.pop(); + pointer.setEvent(event, touchEvent.pageX, touchEvent.pageY, touchEvent.clientX, touchEvent.clientY, touchEvent.identifier); + normalizedEvents.push(pointer); + } + } else { + // Mouse or PointerEvent + pointer = T_POINTERS.pop(); + pointer.setEvent(event, event.pageX, event.pageY, event.clientX, event.clientY, event.pointerId); + normalizedEvents.push(pointer); + } // if event.isPrimary is defined and false, return + + + if (event.isPrimary === false) { + return normalizedEvents; + } // else use the first entry to simulate mouse event + + + normalizedEvents[0].isPrimary = true; + Object.assign(api.pointer, normalizedEvents[0]); + return normalizedEvents; + } + /** + * mouse event management (mousewheel) + * XXX: mousewheel is deprecated + * @ignore + */ + + + function onMouseWheel(e) { + /* jshint expr:true */ + if (e.target === me.video.renderer.getScreenCanvas()) { + // create a (fake) normalized event object + e.type = "wheel"; // dispatch mouse event to registered object + + return dispatchEvent(normalizeEvent(e)); + } + + return true; + } + /** + * mouse/touch/pointer event management (move) + * @ignore + */ + + + function onMoveEvent(e) { + // dispatch mouse event to registered object + dispatchEvent(normalizeEvent(e)); // do not prevent default on moveEvent : + // - raise a deprectated warning in latest chrome version for touchEvent + // - uncessary for pointer Events + + return true; + } + /** + * mouse/touch/pointer event management (start/down, end/up) + * @ignore + */ + + + function onPointerEvent(e) { + var ret = true; // normalize eventTypes + + normalizeEvent(e); // remember/use the first "primary" normalized event for pointer.bind + + var button = normalizedEvents[0].button; // dispatch event to registered objects + + if (dispatchEvent(normalizedEvents) || api.preventDefault) { + // prevent default action + ret = api._preventDefaultFn(e); + } + + var keycode = api.pointer.bind[button]; // check if mapped to a key + + if (keycode) { + if (POINTER_DOWN.includes(e.type)) { + return api._keydown(e, keycode, button + 1); + } else { + // 'mouseup' or 'touchend' + return api._keyup(e, keycode, button + 1); + } + } + + return ret; + } + /* + * PUBLIC STUFF + */ + + /** + * Pointer information (current position and size)
+ * 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:
+ *
    + *
  • "pointermove"
  • + *
  • "pointerdown"
  • + *
  • "pointerup"
  • + *
  • "pointerenter"
  • + *
  • "pointerover"
  • + *
  • "pointerleave"
  • + *
  • "pointercancel"
  • + *
  • "wheel"
  • + *
+ * @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
+ *
    + *
  • Left control stick: LX (horizontal), LY (vertical)
  • + *
  • Right control stick: RX (horizontal), RY (vertical)
  • + *
  • Extras: EXTRA_1, EXTRA_2, EXTRA_3, EXTRA_4
  • + *
+ * @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 buttons
+ *
    + *
  • Face buttons: FACE_1, FACE_2, FACE_3, FACE_4
  • + *
  • D-Pad: UP, DOWN, LEFT, RIGHT
  • + *
  • Shoulder buttons: L1, L2, R1, R2
  • + *
  • Analog stick (clicks): L3, R3
  • + *
  • Navigation: SELECT (BACK), START (FORWARD), HOME
  • + *
  • Extras: 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 inputs
+ * 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

+ * 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
+ * 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.Linear.None
+ * 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 + *

+ * @public + * @constant + * @type enum + * @name Easing + * @memberOf me.Tween + */ + + + me.Tween.Easing = { + Linear: { + /** @ignore */ + None: function None(k) { + return k; + } + }, + Quadratic: { + /** @ignore */ + In: function In(k) { + return k * k; + }, + + /** @ignore */ + Out: function Out(k) { + return k * (2 - k); + }, + + /** @ignore */ + InOut: function InOut(k) { + if ((k *= 2) < 1) return 0.5 * k * k; + return -0.5 * (--k * (k - 2) - 1); + } + }, + Cubic: { + /** @ignore */ + In: function In(k) { + return k * k * k; + }, + + /** @ignore */ + Out: function Out(k) { + return --k * k * k + 1; + }, + + /** @ignore */ + InOut: function InOut(k) { + if ((k *= 2) < 1) return 0.5 * k * k * k; + return 0.5 * ((k -= 2) * k * k + 2); + } + }, + Quartic: { + /** @ignore */ + In: function In(k) { + return k * k * k * k; + }, + + /** @ignore */ + Out: function Out(k) { + return 1 - --k * k * k * k; + }, + + /** @ignore */ + InOut: function InOut(k) { + if ((k *= 2) < 1) return 0.5 * k * k * k * k; + return -0.5 * ((k -= 2) * k * k * k - 2); + } + }, + Quintic: { + /** @ignore */ + In: function In(k) { + return k * k * k * k * k; + }, + + /** @ignore */ + Out: function Out(k) { + return --k * k * k * k * k + 1; + }, + + /** @ignore */ + InOut: function InOut(k) { + if ((k *= 2) < 1) return 0.5 * k * k * k * k * k; + return 0.5 * ((k -= 2) * k * k * k * k + 2); + } + }, + Sinusoidal: { + /** @ignore */ + In: function In(k) { + return 1 - Math.cos(k * Math.PI / 2); + }, + + /** @ignore */ + Out: function Out(k) { + return Math.sin(k * Math.PI / 2); + }, + + /** @ignore */ + InOut: function InOut(k) { + return 0.5 * (1 - Math.cos(Math.PI * k)); + } + }, + Exponential: { + /** @ignore */ + In: function In(k) { + return k === 0 ? 0 : Math.pow(1024, k - 1); + }, + + /** @ignore */ + Out: function Out(k) { + return k === 1 ? 1 : 1 - Math.pow(2, -10 * k); + }, + + /** @ignore */ + InOut: function InOut(k) { + if (k === 0) return 0; + if (k === 1) return 1; + if ((k *= 2) < 1) return 0.5 * Math.pow(1024, k - 1); + return 0.5 * (-Math.pow(2, -10 * (k - 1)) + 2); + } + }, + Circular: { + /** @ignore */ + In: function In(k) { + return 1 - Math.sqrt(1 - k * k); + }, + + /** @ignore */ + Out: function Out(k) { + return Math.sqrt(1 - --k * k); + }, + + /** @ignore */ + InOut: function InOut(k) { + if ((k *= 2) < 1) return -0.5 * (Math.sqrt(1 - k * k) - 1); + return 0.5 * (Math.sqrt(1 - (k -= 2) * k) + 1); + } + }, + Elastic: { + /** @ignore */ + In: function In(k) { + var s, + a = 0.1, + p = 0.4; + if (k === 0) return 0; + if (k === 1) return 1; + + if (!a || a < 1) { + a = 1; + s = p / 4; + } else s = p * Math.asin(1 / a) / (2 * Math.PI); + + return -(a * Math.pow(2, 10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p)); + }, + + /** @ignore */ + Out: function Out(k) { + var s, + a = 0.1, + p = 0.4; + if (k === 0) return 0; + if (k === 1) return 1; + + if (!a || a < 1) { + a = 1; + s = p / 4; + } else s = p * Math.asin(1 / a) / (2 * Math.PI); + + return a * Math.pow(2, -10 * k) * Math.sin((k - s) * (2 * Math.PI) / p) + 1; + }, + + /** @ignore */ + InOut: function InOut(k) { + var s, + a = 0.1, + p = 0.4; + if (k === 0) return 0; + if (k === 1) return 1; + + if (!a || a < 1) { + a = 1; + s = p / 4; + } else s = p * Math.asin(1 / a) / (2 * Math.PI); + + if ((k *= 2) < 1) return -0.5 * (a * Math.pow(2, 10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p)); + return a * Math.pow(2, -10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p) * 0.5 + 1; + } + }, + Back: { + /** @ignore */ + In: function In(k) { + var s = 1.70158; + return k * k * ((s + 1) * k - s); + }, + + /** @ignore */ + Out: function Out(k) { + var s = 1.70158; + return --k * k * ((s + 1) * k + s) + 1; + }, + + /** @ignore */ + InOut: function InOut(k) { + var s = 1.70158 * 1.525; + if ((k *= 2) < 1) return 0.5 * (k * k * ((s + 1) * k - s)); + return 0.5 * ((k -= 2) * k * ((s + 1) * k + s) + 2); + } + }, + Bounce: { + /** @ignore */ + In: function In(k) { + return 1 - me.Tween.Easing.Bounce.Out(1 - k); + }, + + /** @ignore */ + Out: function Out(k) { + if (k < 1 / 2.75) { + return 7.5625 * k * k; + } else if (k < 2 / 2.75) { + return 7.5625 * (k -= 1.5 / 2.75) * k + 0.75; + } else if (k < 2.5 / 2.75) { + return 7.5625 * (k -= 2.25 / 2.75) * k + 0.9375; + } else { + return 7.5625 * (k -= 2.625 / 2.75) * k + 0.984375; + } + }, + + /** @ignore */ + InOut: function InOut(k) { + if (k < 0.5) return me.Tween.Easing.Bounce.In(k * 2) * 0.5; + return me.Tween.Easing.Bounce.Out(k * 2 - 1) * 0.5 + 0.5; + } + } + }; + /** + * Interpolation Function :
+ *

+ * me.Tween.Interpolation.Linear
+ * me.Tween.Interpolation.Bezier
+ * me.Tween.Interpolation.CatmullRom + *

+ * @public + * @constant + * @type enum + * @name Interpolation + * @memberOf me.Tween + */ + + me.Tween.Interpolation = { + /** @ignore */ + Linear: function Linear(v, k) { + var m = v.length - 1, + f = m * k, + i = Math.floor(f), + fn = me.Tween.Interpolation.Utils.Linear; + if (k < 0) return fn(v[0], v[1], f); + if (k > 1) return fn(v[m], v[m - 1], m - f); + return fn(v[i], v[i + 1 > m ? m : i + 1], f - i); + }, + + /** @ignore */ + Bezier: function Bezier(v, k) { + var b = 0, + n = v.length - 1, + pw = Math.pow, + bn = me.Tween.Interpolation.Utils.Bernstein, + i; + + for (i = 0; i <= n; i++) { + b += pw(1 - k, n - i) * pw(k, i) * v[i] * bn(n, i); + } + + return b; + }, + + /** @ignore */ + CatmullRom: function CatmullRom(v, k) { + var m = v.length - 1, + f = m * k, + i = Math.floor(f), + fn = me.Tween.Interpolation.Utils.CatmullRom; + + if (v[0] === v[m]) { + if (k < 0) i = Math.floor(f = m * (1 + k)); + return fn(v[(i - 1 + m) % m], v[i], v[(i + 1) % m], v[(i + 2) % m], f - i); + } else { + if (k < 0) return v[0] - (fn(v[0], v[0], v[1], v[1], -f) - v[0]); + if (k > 1) return v[m] - (fn(v[m], v[m], v[m - 1], v[m - 1], f - m) - v[m]); + return fn(v[i ? i - 1 : 0], v[i], v[m < i + 1 ? m : i + 1], v[m < i + 2 ? m : i + 2], f - i); + } + }, + Utils: { + /** @ignore */ + Linear: function Linear(p0, p1, t) { + return (p1 - p0) * t + p0; + }, + + /** @ignore */ + Bernstein: function Bernstein(n, i) { + var fc = me.Tween.Interpolation.Utils.Factorial; + return fc(n) / fc(i) / fc(n - i); + }, + + /** @ignore */ + Factorial: function () { + var a = [1]; + return function (n) { + var s = 1, + i; + if (a[n]) return a[n]; + + for (i = n; i > 1; i--) { + s *= i; + } + + return a[n] = s; + }; + }(), + + /** @ignore */ + CatmullRom: function CatmullRom(p0, p1, p2, p3, t) { + var v0 = (p2 - p0) * 0.5, + v1 = (p3 - p1) * 0.5, + t2 = t * t, + t3 = t * t2; + return (2 * p1 - 2 * p2 + v0 + v1) * t3 + (-3 * p1 + 3 * p2 - 2 * v0 - v1) * t2 + v0 * t + p1; + } + } + }; + })(); + /* eslint-enable quotes, keyword-spacing, comma-spacing, no-return-assign */ + + (function () { + /** + * There is no constructor function for me.plugins
+ * 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;ewindow.outerHeight?"landscape":"portrait"},e.lockOrientation=function(e){var t=window.screen;if(void 0!==t){var i=me.agent.prefixed("lockOrientation",t);if(void 0!==i)return i(e)}return!1},e.unlockOrientation=function(e){var t=window.screen;if(void 0!==t){var i=me.agent.prefixed("unlockOrientation",t);if(void 0!==i)return i(e)}return!1},e.isPortrait=function(){return me.device.getScreenOrientation().includes("portrait")},e.isLandscape=function(){return me.device.getScreenOrientation().includes("landscape")},e.getStorage=function(e){switch(e=e||"local"){case"local":return me.save;default:throw new Error("storage type "+e+" not supported")}},e.getMaxShaderPrecision=function(e){return e.getShaderPrecisionFormat(e.VERTEX_SHADER,e.HIGH_FLOAT).precision>0&&e.getShaderPrecisionFormat(e.FRAGMENT_SHADER,e.HIGH_FLOAT).precision>0?"highp":e.getShaderPrecisionFormat(e.VERTEX_SHADER,e.MEDIUM_FLOAT).precision>0&&e.getShaderPrecisionFormat(e.FRAGMENT_SHADER,e.MEDIUM_FLOAT).precision>0?"mediump":"lowp"},e.focus=function(){"function"==typeof window.focus&&window.focus()},e.turnOnPointerLock=function(){if(this.hasPointerLockSupport){var e=me.video.getWrapper();if(me.device.ua.match(/Firefox/i)){var t=function t(){(me.agent.prefixed("fullscreenElement",document)||document.mozFullScreenElement)===e&&(document.removeEventListener("fullscreenchange",t),document.removeEventListener("mozfullscreenchange",t),e.requestPointerLock=me.agent.prefixed("requestPointerLock",e),e.requestPointerLock())};document.addEventListener("fullscreenchange",t,!1),document.addEventListener("mozfullscreenchange",t,!1),me.device.requestFullscreen()}else e.requestPointerLock()}},e.turnOffPointerLock=function(){this.hasPointerLockSupport&&document.exitPointerLock()},e.watchAccelerometer=function(){return!!me.device.hasAccelerometer&&(t||(window.addEventListener("devicemotion",h,!1),t=!0),!0)},e.unwatchAccelerometer=function(){t&&(window.removeEventListener("devicemotion",h,!1),t=!1)},e.watchDeviceOrientation=function(){return me.device.hasDeviceOrientation&&!i&&(window.addEventListener("deviceorientation",l,!1),i=!0),!1},e.unwatchDeviceOrientation=function(){i&&(window.removeEventListener("deviceorientation",l,!1),i=!1)},e.vibrate=function(e){navigator.vibrate&&navigator.vibrate(e)},e}(),Object.defineProperty(me.device,"devicePixelRatio",{get:function(){return window.devicePixelRatio||1}}),Object.defineProperty(me.device,"isFullscreen",{get:function(){return!(!me.device.hasFullscreenSupport||!me.agent.prefixed("fullscreenElement",document)&&!document.mozFullScreenElement)}}),Object.defineProperty(me.device,"sound",{get:function(){return me.audio.hasAudio()}});var minpubsub_src=createCommonjsModule(function(e,t){ +/*! + * MinPubSub + * Copyright(c) 2011 Daniel Lamb + * MIT Licensed + */ +var i,n,r;i=commonjsGlobal.window,n={},r=i.c_||{},n.publish=function(e,t){for(var n=r[e],s=n?n.length:0;s--;)n[s].apply(i,t||[])},n.subscribe=function(e,t){return r[e]||(r[e]=[]),r[e].push(t),[e,t]},n.unsubscribe=function(e,t){for(var i=r[t?e:e[0]],n=(t=t||e[1],i?i.length:0);n--;)i[n]===t&&i.splice(n,1)},e.exports?e.exports=n:"object"==typeof i&&(i.publish=n.publish,i.subscribe=n.subscribe,i.unsubscribe=n.unsubscribe)});!function(){var e;me.event=((e={STATE_PAUSE:"me.state.onPause",STATE_RESUME:"me.state.onResume",STATE_STOP:"me.state.onStop",STATE_RESTART:"me.state.onRestart",GAME_INIT:"me.game.onInit",GAME_RESET:"me.game.onReset",LEVEL_LOADED:"me.game.onLevelLoaded",LOADER_COMPLETE:"me.loader.onload",LOADER_PROGRESS:"me.loader.onProgress",KEYDOWN:"me.input.keydown",KEYUP:"me.input.keyup",GAMEPAD_CONNECTED:"gamepad.connected",GAMEPAD_DISCONNECTED:"gamepad.disconnected",GAMEPAD_UPDATE:"gamepad.update",POINTERMOVE:"me.event.pointermove",DRAGSTART:"me.game.dragstart",DRAGEND:"me.game.dragend",WINDOW_ONRESIZE:"window.onresize",CANVAS_ONRESIZE:"canvas.onresize",VIEWPORT_ONRESIZE:"viewport.onresize",WINDOW_ONORIENTATION_CHANGE:"window.orientationchange",WINDOW_ONSCROLL:"window.onscroll",VIEWPORT_ONCHANGE:"viewport.onchange",WEBGL_ONCONTEXT_LOST:"renderer.webglcontextlost",WEBGL_ONCONTEXT_RESTORED:"renderer.webglcontextrestored"}).publish=minpubsub_src.publish,e.subscribe=minpubsub_src.subscribe,e.unsubscribe=minpubsub_src.unsubscribe,e)}(),function(){var e,t,i,n,r,s,o,a,h,l,u,c,d,f;me.game=(t=!1,i=!0,n=!1,r=0,s=1,o=0,a=0,h=0,l=1e3/60,u=0,c=null,d=0,f=null,(e={}).viewport=void 0,e.world=null,e.mergeGroup=!0,e.sortOn="z",e.onLevelLoaded=function(){},e.HASH=null,e.init=function(n,r){t||(n=n||me.video.renderer.getWidth(),r=r||me.video.renderer.getHeight(),e.world=new me.Container(0,0,n,r,!0),e.world.name="rootContainer",e.world.anchorPoint.set(0,0),me.collision.init(),f=me.video.renderer,me.event.publish(me.event.GAME_INIT),i=!0,t=!0)},e.reset=function(){me.collision.quadTree.clear(),e.world.reset(),e.world.anchorPoint.set(0,0),e.viewport=me.state.current().cameras.get("default"),me.event.publish(me.event.GAME_RESET),e.updateFrameRate()},e.updateFrameRate=function(){r=0,s=~~(.5+60/me.sys.fps),l=1e3/me.sys.updatesPerSecond,o=0,a=10*l,n=me.sys.fps>me.sys.updatesPerSecond},e.getParentContainer=function(e){return e.ancestor},e.repaint=function(){i=!0},e.update=function(t,n){if(++r%s==0)for(r=0,me.timer.update(t),me.input._updateGamepads(),o+=me.timer.getDelta(),o=Math.min(o,a),u=me.sys.interpolation?me.timer.getDelta():l,h=me.sys.interpolation?u:Math.max(u,d);o>=h||me.sys.interpolation;)if(c=window.performance.now(),me.collision.quadTree.clear(),me.collision.quadTree.insertContainer(e.world),i=e.world.update(u)||i,n.cameras.forEach(function(e){e.update(u)&&(i=!0)}),me.timer.lastUpdate=window.performance.now(),d=me.timer.lastUpdate-c,o-=h,me.sys.interpolation){o=0;break}},e.draw=function(e){!0===f.isContextValid&&(i||n)&&(f.clear(),e.cameras.forEach(function(e){e.draw(f,me.game.world)}),i=!1,f.flush())},e)}(),function(){me.mod="melonJS",me.version="7.0.0",me.sys={fps:60,updatesPerSecond:60,interpolation:!1,scale:null,gravity:void 0,stopOnAudioError:!0,pauseOnBlur:!0,resumeOnFocus:!0,autoFocus:!0,stopOnBlur:!1,preRender:!1,checkVersion:function(e,t){t=t||me.version;for(var i=e.split("."),n=t.split("."),r=Math.min(i.length,n.length),s=0,o=0;o3?Array.prototype.slice.call(arguments,3):void 0}),l},e.setInterval=function(e,t,i){return h.push({fn:e,delay:t,elapsed:0,repeat:!0,timerId:++l,pauseable:!0===i||!0,args:arguments.length>3?Array.prototype.slice.call(arguments,3):void 0}),l},e.clearTimeout=function(e){me.utils.function.defer(u,this,e)},e.clearInterval=function(e){me.utils.function.defer(u,this,e)},e.getTime=function(){return r},e.getDelta=function(){return s},e.countFPS=function(){i+=s,++t%10==0&&(this.fps=me.Math.clamp(~~(1e3*t/i),0,me.sys.fps),i=0,t=0)},e.update=function(t){return n=r,(s=(r=t)-n)<0&&(s=0),e.tick=s>a&&me.sys.interpolation?s/o:1,function(e){for(var t=0,i=h.length;t=n.delay&&(n.fn.apply(null,n.args),!0===n.repeat?n.elapsed-=n.delay:me.timer.clearTimeout(n.timerId))}}(s),s},e)}(),function(){var e,t,i;me.pool=(t={},i=0,(e={}).init=function(){e.register("me.Entity",me.Entity),e.register("me.CollectableEntity",me.CollectableEntity),e.register("me.LevelEntity",me.LevelEntity),e.register("me.Tween",me.Tween,!0),e.register("me.Color",me.Color,!0),e.register("me.Particle",me.Particle,!0),e.register("me.Sprite",me.Sprite),e.register("me.Text",me.Text,!0),e.register("me.BitmapText",me.BitmapText,!0),e.register("me.BitmapTextData",me.BitmapTextData,!0),e.register("me.ImageLayer",me.ImageLayer,!0),e.register("me.ColorLayer",me.ColorLayer,!0),e.register("me.Vector2d",me.Vector2d,!0),e.register("me.Vector3d",me.Vector3d,!0),e.register("me.ObservableVector2d",me.ObservableVector2d,!0),e.register("me.ObservableVector3d",me.ObservableVector3d,!0),e.register("me.Matrix2d",me.Matrix2d,!0),e.register("me.Rect",me.Rect,!0),e.register("me.Polygon",me.Polygon,!0),e.register("me.Line",me.Line,!0),e.register("me.Ellipse",me.Ellipse,!0)},e.register=function(e,i,n){if(void 0===i)throw new Error("Cannot register object '"+e+"', invalid class");t[e]={class:i,pool:n?[]:void 0}},e.pull=function(e){for(var n=new Array(arguments.length),r=0;r>1,e|=e>>2,e|=e>>4,e|=e>>8,e|=e>>16,++e},e.degToRad=function(t){return t*e.DEG_TO_RAD},e.radToDeg=function(t){return t*e.RAD_TO_DEG},e.clamp=function(e,t,i){return ei?i:+e},e.random=function(e,t){return~~(Math.random()*(t-e))+e},e.randomFloat=function(e,t){return Math.random()*(t-e)+e},e.weightedRandom=function(e,t){return~~(Math.pow(Math.random(),2)*(t-e))+e},e.round=function(e,t){var i=Math.pow(10,t||0);return~~(.5+e*i)/i},e.toBeCloseTo=function(e,t,i){return"number"!=typeof i&&(i=2),Math.abs(e-t)e.x?this.x:e.x,this.y>e.y?this.y:e.y)},floor:function(){return new me.Vector2d(Math.floor(this.x),Math.floor(this.y))},floorSelf:function(){return this._set(Math.floor(this.x),Math.floor(this.y))},ceil:function(){return new me.Vector2d(Math.ceil(this.x),Math.ceil(this.y))},ceilSelf:function(){return this._set(Math.ceil(this.x),Math.ceil(this.y))},negate:function(){return new me.Vector2d(-this.x,-this.y)},negateSelf:function(){return this._set(-this.x,-this.y)},copy:function(e){return this._set(e.x,e.y)},equals:function(e){return this.x===e.x&&this.y===e.y},normalize:function(){var e=this.length();return e>0?this._set(this.x/e,this.y/e):this},perp:function(){return this._set(this.y,-this.x)},rotate:function(e){var t=this.x,i=this.y;return this._set(t*Math.cos(e)-i*Math.sin(e),t*Math.sin(e)+i*Math.cos(e))},dotProduct:function(e){return this.x*e.x+this.y*e.y},length2:function(){return this.dotProduct(this)},length:function(){return Math.sqrt(this.length2())},lerp:function(e,t){return this.x+=(e.x-this.x)*t,this.y+=(e.y-this.y)*t,this},distance:function(e){var t=this.x-e.x,i=this.y-e.y;return Math.sqrt(t*t+i*i)},angle:function(e){return Math.acos(me.Math.clamp(this.dotProduct(e)/(this.length()*e.length()),-1,1))},project:function(e){return this.scale(this.dotProduct(e)/e.length2())},projectN:function(e){return this.scale(this.dotProduct(e))},clone:function(){return me.pool.pull("me.Vector2d",this.x,this.y)},toString:function(){return"x:"+this.x+",y:"+this.y}}),me.Vector3d=me.Object.extend({init:function(e,t,i){return this.set(e||0,t||0,i||0)},_set:function(e,t,i){return this.x=e,this.y=t,this.z=i,this},set:function(e,t,i){if(e!==+e||t!==+t||i!==+i)throw new Error("invalid x, y, z parameters (not a number)");return this._set(e,t,i)},setZero:function(){return this.set(0,0,0)},setV:function(e){return this._set(e.x,e.y,void 0!==e.z?e.z:this.z)},add:function(e){return this._set(this.x+e.x,this.y+e.y,this.z+(e.z||0))},sub:function(e){return this._set(this.x-e.x,this.y-e.y,this.z-(e.z||0))},scale:function(e,t,i){return t=void 0!==t?t:e,i=void 0!==i?i:e,this._set(this.x*e,this.y*t,this.z*i)},scaleV:function(e){return this._set(this.x*e.x,this.y*e.y,this.z*(e.z||1))},toIso:function(){return this._set(this.x-this.y,.5*(this.x+this.y),this.z)},to2d:function(){return this._set(this.y+this.x/2,this.y-this.x/2,this.z)},div:function(e){return this._set(this.x/e,this.y/e,this.z/e)},abs:function(){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:function(e,t){return new me.Vector3d(me.Math.clamp(this.x,e,t),me.Math.clamp(this.y,e,t),me.Math.clamp(this.z,e,t))},clampSelf:function(e,t){return this._set(me.Math.clamp(this.x,e,t),me.Math.clamp(this.y,e,t),me.Math.clamp(this.z,e,t))},minV:function(e){var t=e.z||0;return this._set(this.xe.x?this.x:e.x,this.y>e.y?this.y:e.y,this.z>t?this.z:t)},floor:function(){return new me.Vector3d(Math.floor(this.x),Math.floor(this.y),Math.floor(this.z))},floorSelf:function(){return this._set(Math.floor(this.x),Math.floor(this.y),Math.floor(this.z))},ceil:function(){return new me.Vector3d(Math.ceil(this.x),Math.ceil(this.y),Math.ceil(this.z))},ceilSelf:function(){return this._set(Math.ceil(this.x),Math.ceil(this.y),Math.ceil(this.z))},negate:function(){return new me.Vector3d(-this.x,-this.y,-this.z)},negateSelf:function(){return this._set(-this.x,-this.y,-this.z)},copy:function(e){return this._set(e.x,e.y,void 0!==e.z?e.z:this.z)},equals:function(e){return this.x===e.x&&this.y===e.y&&this.z===(e.z||this.z)},normalize:function(){var e=this.length();return e>0?this._set(this.x/e,this.y/e,this.z/e):this},perp:function(){return this._set(this.y,-this.x,this.z)},rotate:function(e){var t=this.x,i=this.y;return this._set(t*Math.cos(e)-i*Math.sin(e),t*Math.sin(e)+i*Math.cos(e),this.z)},dotProduct:function(e){return this.x*e.x+this.y*e.y+this.z*(e.z||1)},length2:function(){return this.dotProduct(this)},length:function(){return Math.sqrt(this.length2())},lerp:function(e,t){return this.x+=(e.x-this.x)*t,this.y+=(e.y-this.y)*t,this.z+=(e.z-this.z)*t,this},distance:function(e){var t=this.x-e.x,i=this.y-e.y,n=this.z-(e.z||0);return Math.sqrt(t*t+i*i+n*n)},angle:function(e){return Math.acos(me.Math.clamp(this.dotProduct(e)/(this.length()*e.length()),-1,1))},project:function(e){return this.scale(this.dotProduct(e)/e.length2())},projectN:function(e){return this.scale(this.dotProduct(e))},clone:function(){return me.pool.pull("me.Vector3d",this.x,this.y,this.z)},toString:function(){return"x:"+this.x+",y:"+this.y+",z:"+this.z}}),me.ObservableVector2d=me.Vector2d.extend({init:function(e,t,i){if(Object.defineProperty(this,"x",{get:function(){return this._x},set:function(e){var t=this.onUpdate(e,this._y,this._x,this._y);this._x=t&&"x"in t?t.x:e},configurable:!0}),Object.defineProperty(this,"y",{get:function(){return this._y},set:function(e){var t=this.onUpdate(this._x,e,this._x,this._y);this._y=t&&"y"in t?t.y:e},configurable:!0}),void 0===i)throw new Error("undefined `onUpdate` callback");this.setCallback(i.onUpdate),this._x=e||0,this._y=t||0},_set:function(e,t){var i=this.onUpdate(e,t,this._x,this._y);return i&&"x"in i&&"y"in i?(this._x=i.x,this._y=i.y):(this._x=e,this._y=t),this},setMuted:function(e,t){return this._x=e,this._y=t,this},setCallback:function(e){if("function"!=typeof e)throw new Error("invalid `onUpdate` callback");return this.onUpdate=e,this},add:function(e){return this._set(this._x+e.x,this._y+e.y)},sub:function(e){return this._set(this._x-e.x,this._y-e.y)},scale:function(e,t){return this._set(this._x*e,this._y*(void 0!==t?t:e))},scaleV:function(e){return this._set(this._x*e.x,this._y*e.y)},div:function(e){return this._set(this._x/e,this._y/e)},abs:function(){return this._set(this._x<0?-this._x:this._x,this._y<0?-this._y:this._y)},clamp:function(e,t){return new me.ObservableVector2d(me.Math.clamp(this.x,e,t),me.Math.clamp(this.y,e,t),{onUpdate:this.onUpdate})},clampSelf:function(e,t){return this._set(me.Math.clamp(this._x,e,t),me.Math.clamp(this._y,e,t))},minV:function(e){return this._set(this._xe.x?this._x:e.x,this._y>e.y?this._y:e.y)},floor:function(){return new me.ObservableVector2d(Math.floor(this._x),Math.floor(this._y),{onUpdate:this.onUpdate})},floorSelf:function(){return this._set(Math.floor(this._x),Math.floor(this._y))},ceil:function(){return new me.ObservableVector2d(Math.ceil(this._x),Math.ceil(this._y),{onUpdate:this.onUpdate})},ceilSelf:function(){return this._set(Math.ceil(this._x),Math.ceil(this._y))},negate:function(){return new me.ObservableVector2d(-this._x,-this._y,{onUpdate:this.onUpdate})},negateSelf:function(){return this._set(-this._x,-this._y)},copy:function(e){return this._set(e.x,e.y)},equals:function(e){return this._x===e.x&&this._y===e.y},normalize:function(){var e=this.length();return e>0?this._set(this._x/e,this._y/e):this},perp:function(){return this._set(this._y,-this._x)},rotate:function(e){var t=this._x,i=this._y;return this._set(t*Math.cos(e)-i*Math.sin(e),t*Math.sin(e)+i*Math.cos(e))},dotProduct:function(e){return this._x*e.x+this._y*e.y},lerp:function(e,t){return this._x+=(e.x-this._x)*t,this._y+=(e.y-this._y)*t,this},distance:function(e){return Math.sqrt((this._x-e.x)*(this._x-e.x)+(this._y-e.y)*(this._y-e.y))},clone:function(){return me.pool.pull("me.ObservableVector2d",this._x,this._y,{onUpdate:this.onUpdate})},toVector2d:function(){return me.pool.pull("me.Vector2d",this._x,this._y)},toString:function(){return"x:"+this._x+",y:"+this._y}}),me.ObservableVector3d=me.Vector3d.extend({init:function(e,t,i,n){if(Object.defineProperty(this,"x",{get:function(){return this._x},set:function(e){var t=this.onUpdate(e,this._y,this._z,this._x,this._y,this._z);this._x=t&&"x"in t?t.x:e},configurable:!0}),Object.defineProperty(this,"y",{get:function(){return this._y},set:function(e){var t=this.onUpdate(this._x,e,this._z,this._x,this._y,this._z);this._y=t&&"y"in t?t.y:e},configurable:!0}),Object.defineProperty(this,"z",{get:function(){return this._z},set:function(e){var t=this.onUpdate(this._x,this._y,e,this._x,this._y,this._z);this._z=t&&"z"in t?t.z:e},configurable:!0}),void 0===n)throw new Error("undefined `onUpdate` callback");this.setCallback(n.onUpdate),this._x=e||0,this._y=t||0,this._z=i||0},_set:function(e,t,i){var n=this.onUpdate(e,t,i,this._x,this._y,this._z);return n&&"x"in n&&"y"in n&&"z"in n?(this._x=n.x,this._y=n.y,this._z=n.z):(this._x=e,this._y=t,this._z=i),this},setMuted:function(e,t,i){return this._x=e,this._y=t,this._z=i,this},setCallback:function(e){if("function"!=typeof e)throw new Error("invalid `onUpdate` callback");return this.onUpdate=e,this},add:function(e){return this._set(this._x+e.x,this._y+e.y,this._z+(e.z||0))},sub:function(e){return this._set(this._x-e.x,this._y-e.y,this._z-(e.z||0))},scale:function(e,t,i){return t=void 0!==t?t:e,i=void 0!==i?i:e,this._set(this._x*e,this._y*t,this._z*i)},scaleV:function(e){return this._set(this._x*e.x,this._y*e.y,this._z*(e.z||1))},div:function(e){return this._set(this._x/e,this._y/e,this._z/e)},abs:function(){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:function(e,t){return new me.ObservableVector3d(me.Math.clamp(this._x,e,t),me.Math.clamp(this._y,e,t),me.Math.clamp(this._z,e,t),{onUpdate:this.onUpdate})},clampSelf:function(e,t){return this._set(me.Math.clamp(this._x,e,t),me.Math.clamp(this._y,e,t),me.Math.clamp(this._z,e,t))},minV:function(e){var t=e.z||0;return this._set(this._xe.x?this._x:e.x,this._y>e.y?this._y:e.y,this._z>t?this._z:t)},floor:function(){return new me.ObservableVector3d(Math.floor(this._x),Math.floor(this._y),Math.floor(this._z),{onUpdate:this.onUpdate})},floorSelf:function(){return this._set(Math.floor(this._x),Math.floor(this._y),Math.floor(this._z))},ceil:function(){return new me.ObservableVector3d(Math.ceil(this._x),Math.ceil(this._y),Math.ceil(this._z),{onUpdate:this.onUpdate})},ceilSelf:function(){return this._set(Math.ceil(this._x),Math.ceil(this._y),Math.ceil(this._z))},negate:function(){return new me.ObservableVector3d(-this._x,-this._y,-this._z,{onUpdate:this.onUpdate})},negateSelf:function(){return this._set(-this._x,-this._y,-this._z)},copy:function(e){return this._set(e.x,e.y,void 0!==e.z?e.z:this._z)},equals:function(e){return this._x===e.x&&this._y===e.y&&this._z===(e.z||this._z)},normalize:function(){var e=this.length();return e>0?this._set(this._x/e,this._y/e,this._z/e):this},perp:function(){return this._set(this._y,-this._x,this._z)},rotate:function(e){var t=this._x,i=this._y;return this._set(t*Math.cos(e)-i*Math.sin(e),t*Math.sin(e)+i*Math.cos(e),this._z)},dotProduct:function(e){return this._x*e.x+this._y*e.y+this._z*(e.z||1)},lerp:function(e,t){return this._x+=(e.x-this._x)*t,this._y+=(e.y-this._y)*t,this._z+=(e.z-this._z)*t,this},distance:function(e){var t=this._x-e.x,i=this._y-e.y,n=this._z-(e.z||0);return Math.sqrt(t*t+i*i+n*n)},clone:function(){return me.pool.pull("me.ObservableVector3d",this._x,this._y,this._z,{onUpdate:this.onUpdate})},toVector3d:function(){return me.pool.pull("me.Vector3d",this._x,this._y,this._z)},toString:function(){return"x:"+this._x+",y:"+this._y+",z:"+this._z}}),me.Matrix2d=me.Object.extend({init:function(){void 0===this.val&&(this.val=new Float32Array(9)),arguments.length&&arguments[0]instanceof me.Matrix2d?this.copy(arguments[0]):arguments.length>=6?this.setTransform.apply(this,arguments):this.identity()},identity:function(){return this.setTransform(1,0,0,0,1,0,0,0,1),this},setTransform:function(){var e=this.val;return 9===arguments.length?(e[0]=arguments[0],e[1]=arguments[1],e[2]=arguments[2],e[3]=arguments[3],e[4]=arguments[4],e[5]=arguments[5],e[6]=arguments[6],e[7]=arguments[7],e[8]=arguments[8]):6===arguments.length&&(e[0]=arguments[0],e[1]=arguments[2],e[2]=arguments[4],e[3]=arguments[1],e[4]=arguments[3],e[5]=arguments[5],e[6]=0,e[7]=0,e[8]=1),this},copy:function(e){return this.val.set(e.val),this},multiply:function(e){e=e.val;var t=this.val,i=t[0],n=t[1],r=t[3],s=t[4],o=e[0],a=e[1],h=e[3],l=e[4],u=e[6],c=e[7];return t[0]=i*o+r*a,t[1]=n*o+s*a,t[3]=i*h+r*l,t[4]=n*h+s*l,t[6]+=i*u+r*c,t[7]+=n*u+s*c,this},transpose:function(){var e,t=this.val;return e=t[1],t[1]=t[3],t[3]=e,e=t[2],t[2]=t[6],t[6]=e,e=t[5],t[5]=t[7],t[7]=e,this},invert:function(){var e=this.val,t=e[0],i=e[1],n=e[2],r=e[3],s=e[4],o=e[5],a=e[6],h=e[7],l=e[8],u=l*s-o*h,c=o*a-l*r,d=h*r-s*a,f=t*u+i*c+n*d;return e[0]=u/f,e[1]=(n*h-l*i)/f,e[2]=(o*i-n*s)/f,e[3]=c/f,e[4]=(l*t-n*a)/f,e[5]=(n*r-o*t)/f,e[6]=d/f,e[7]=(i*a-h*t)/f,e[8]=(s*t-i*r)/f,this},multiplyVector:function(e){var t=this.val,i=e.x,n=e.y;return e.x=i*t[0]+n*t[3]+t[6],e.y=i*t[1]+n*t[4]+t[7],e},multiplyVectorInverse:function(e){var t=this.val,i=e.x,n=e.y,r=1/(t[0]*t[4]+t[3]*-t[1]);return e.x=t[4]*r*i+-t[3]*r*n+(t[7]*t[3]-t[6]*t[4])*r,e.y=t[0]*r*n+-t[1]*r*i+(-t[7]*t[0]+t[6]*t[1])*r,e},scale:function(e,t){var i=this.val,n=e,r=void 0===t?n:t;return i[0]*=n,i[1]*=n,i[3]*=r,i[4]*=r,this},scaleV:function(e){return this.scale(e.x,e.y)},scaleX:function(e){return this.scale(e,1)},scaleY:function(e){return this.scale(1,e)},rotate:function(e){if(0!==e){var t=this.val,i=t[0],n=t[1],r=t[3],s=t[4],o=Math.sin(e),a=Math.cos(e);t[0]=i*a+r*o,t[1]=n*a+s*o,t[3]=i*-o+r*a,t[4]=n*-o+s*a}return this},translate:function(e,t){var i=this.val;return i[6]+=i[0]*e+i[3]*t,i[7]+=i[1]*e+i[4]*t,this},translateV:function(e){return this.translate(e.x,e.y)},isIdentity:function(){var e=this.val;return 1===e[0]&&0===e[1]&&0===e[2]&&0===e[3]&&1===e[4]&&0===e[5]&&0===e[6]&&0===e[7]&&1===e[8]},clone:function(){return me.pool.pull("me.Matrix2d",this)},toString:function(){var e=this.val;return"me.Matrix2d("+e[0]+", "+e[1]+", "+e[2]+", "+e[3]+", "+e[4]+", "+e[5]+", "+e[6]+", "+e[7]+", "+e[8]+")"}}),Object.defineProperty(me.Matrix2d.prototype,"tx",{get:function(){return this.val[6]},configurable:!0}),Object.defineProperty(me.Matrix2d.prototype,"ty",{get:function(){return this.val[7]},configurable:!0}),me.Ellipse=me.Object.extend({init:function(e,t,i,n){this.pos=new me.Vector2d,this._bounds=void 0,this.radius=NaN,this.radiusV=new me.Vector2d,this.radiusSq=new me.Vector2d,this.ratio=new me.Vector2d,this.shapeType="Ellipse",this.setShape(e,t,i,n)},onResetEvent:function(e,t,i,n){this.setShape(e,t,i,n)},setShape:function(e,t,i,n){var r=i/2,s=n/2;this.pos.set(e,t),this.radius=Math.max(r,s),this.ratio.set(r/this.radius,s/this.radius),this.radiusV.set(this.radius,this.radius).scaleV(this.ratio);var o=this.radius*this.radius;return this.radiusSq.set(o,o).scaleV(this.ratio),this.updateBounds(),this},rotate:function(){return this},scale:function(e,t){return t=void 0!==t?t:e,this.setShape(this.pos.x,this.pos.y,2*this.radiusV.x*e,2*this.radiusV.y*t)},scaleV:function(e){return this.scale(e.x,e.y)},transform:function(){return this},translate:function(e,t){return this.pos.x+=e,this.pos.y+=t,this._bounds.translate(e,t),this},translateV:function(e){return this.pos.add(e),this._bounds.translateV(e),this},containsPointV:function(e){return this.containsPoint(e.x,e.y)},containsPoint:function(e,t){return e-=this.pos.x,t-=this.pos.y,e*e/this.radiusSq.x+t*t/this.radiusSq.y<=1},getBounds:function(){return this._bounds},updateBounds:function(){var e=this.radiusV.x,t=this.radiusV.y,i=this.pos.x-e,n=this.pos.y-t,r=2*e,s=2*t;return this._bounds?this._bounds.setShape(i,n,r,s):this._bounds=new me.Rect(i,n,r,s),this._bounds},clone:function(){return new me.Ellipse(this.pos.x,this.pos.y,2*this.radiusV.x,2*this.radiusV.y)}});var earcut_1=earcut,default_1=earcut,deferredRemove,globalFloatingCounter,MIN,MAX,targetV,default_camera,default_settings,ProgressBar,IconLogo,TextLogo,runits,toPX,setContextStyle,measureTextWidth,measureTextHeight,xChars,capChars,Glyph;function earcut(e,t,i){i=i||2;var n,r,s,o,a,h,l,u=t&&t.length,c=u?t[0]*i:e.length,d=linkedList(e,0,c,i,!0),f=[];if(!d||d.next===d.prev)return f;if(u&&(d=eliminateHoles(e,t,d,i)),e.length>80*i){n=s=e[0],r=o=e[1];for(var p=i;ps&&(s=a),h>o&&(o=h);l=0!==(l=Math.max(s-n,o-r))?1/l:0}return earcutLinked(d,f,i,n,r,l),f}function linkedList(e,t,i,n,r){var s,o;if(r===signedArea(e,t,i,n)>0)for(s=t;s=t;s-=n)o=insertNode(s,e[s],e[s+1],o);return o&&equals(o,o.next)&&(removeNode(o),o=o.next),o}function filterPoints(e,t){if(!e)return e;t||(t=e);var i,n=e;do{if(i=!1,n.steiner||!equals(n,n.next)&&0!==area(n.prev,n,n.next))n=n.next;else{if(removeNode(n),(n=t=n.prev)===n.next)break;i=!0}}while(i||n!==t);return t}function earcutLinked(e,t,i,n,r,s,o){if(e){!o&&s&&indexCurve(e,n,r,s);for(var a,h,l=e;e.prev!==e.next;)if(a=e.prev,h=e.next,s?isEarHashed(e,n,r,s):isEar(e))t.push(a.i/i),t.push(e.i/i),t.push(h.i/i),removeNode(e),e=h.next,l=h.next;else if((e=h)===l){o?1===o?earcutLinked(e=cureLocalIntersections(e,t,i),t,i,n,r,s,2):2===o&&splitEarcut(e,t,i,n,r,s):earcutLinked(filterPoints(e),t,i,n,r,s,1);break}}}function isEar(e){var t=e.prev,i=e,n=e.next;if(area(t,i,n)>=0)return!1;for(var r=e.next.next;r!==e.prev;){if(pointInTriangle(t.x,t.y,i.x,i.y,n.x,n.y,r.x,r.y)&&area(r.prev,r,r.next)>=0)return!1;r=r.next}return!0}function isEarHashed(e,t,i,n){var r=e.prev,s=e,o=e.next;if(area(r,s,o)>=0)return!1;for(var a=r.xs.x?r.x>o.x?r.x:o.x:s.x>o.x?s.x:o.x,u=r.y>s.y?r.y>o.y?r.y:o.y:s.y>o.y?s.y:o.y,c=zOrder(a,h,t,i,n),d=zOrder(l,u,t,i,n),f=e.prevZ,p=e.nextZ;f&&f.z>=c&&p&&p.z<=d;){if(f!==e.prev&&f!==e.next&&pointInTriangle(r.x,r.y,s.x,s.y,o.x,o.y,f.x,f.y)&&area(f.prev,f,f.next)>=0)return!1;if(f=f.prevZ,p!==e.prev&&p!==e.next&&pointInTriangle(r.x,r.y,s.x,s.y,o.x,o.y,p.x,p.y)&&area(p.prev,p,p.next)>=0)return!1;p=p.nextZ}for(;f&&f.z>=c;){if(f!==e.prev&&f!==e.next&&pointInTriangle(r.x,r.y,s.x,s.y,o.x,o.y,f.x,f.y)&&area(f.prev,f,f.next)>=0)return!1;f=f.prevZ}for(;p&&p.z<=d;){if(p!==e.prev&&p!==e.next&&pointInTriangle(r.x,r.y,s.x,s.y,o.x,o.y,p.x,p.y)&&area(p.prev,p,p.next)>=0)return!1;p=p.nextZ}return!0}function cureLocalIntersections(e,t,i){var n=e;do{var r=n.prev,s=n.next.next;!equals(r,s)&&intersects(r,n,n.next,s)&&locallyInside(r,s)&&locallyInside(s,r)&&(t.push(r.i/i),t.push(n.i/i),t.push(s.i/i),removeNode(n),removeNode(n.next),n=e=s),n=n.next}while(n!==e);return n}function splitEarcut(e,t,i,n,r,s){var o=e;do{for(var a=o.next.next;a!==o.prev;){if(o.i!==a.i&&isValidDiagonal(o,a)){var h=splitPolygon(o,a);return o=filterPoints(o,o.next),h=filterPoints(h,h.next),earcutLinked(o,t,i,n,r,s),void earcutLinked(h,t,i,n,r,s)}a=a.next}o=o.next}while(o!==e)}function eliminateHoles(e,t,i,n){var r,s,o,a=[];for(r=0,s=t.length;r=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.x=n.x&&n.x>=u&&r!==n.x&&pointInTriangle(si.x)&&locallyInside(n,e)&&(i=n,d=h),n=n.next;return i}function indexCurve(e,t,i,n){var r=e;do{null===r.z&&(r.z=zOrder(r.x,r.y,t,i,n)),r.prevZ=r.prev,r.nextZ=r.next,r=r.next}while(r!==e);r.prevZ.nextZ=null,r.prevZ=null,sortLinked(r)}function sortLinked(e){var t,i,n,r,s,o,a,h,l=1;do{for(i=e,e=null,s=null,o=0;i;){for(o++,n=i,a=0,t=0;t0||h>0&&n;)0!==a&&(0===h||!n||i.z<=n.z)?(r=i,i=i.nextZ,a--):(r=n,n=n.nextZ,h--),s?s.nextZ=r:e=r,r.prevZ=s,s=r;i=n}s.nextZ=null,l*=2}while(o>1);return e}function zOrder(e,t,i,n,r){return(e=1431655765&((e=858993459&((e=252645135&((e=16711935&((e=32767*(e-i)*r)|e<<8))|e<<4))|e<<2))|e<<1))|(t=1431655765&((t=858993459&((t=252645135&((t=16711935&((t=32767*(t-n)*r)|t<<8))|t<<4))|t<<2))|t<<1))<<1}function getLeftmost(e){var t=e,i=e;do{(t.x=0&&(e-o)*(n-a)-(i-o)*(t-a)>=0&&(i-o)*(s-a)-(r-o)*(n-a)>=0}function isValidDiagonal(e,t){return e.next.i!==t.i&&e.prev.i!==t.i&&!intersectsPolygon(e,t)&&locallyInside(e,t)&&locallyInside(t,e)&&middleInside(e,t)}function area(e,t,i){return(t.y-e.y)*(i.x-t.x)-(t.x-e.x)*(i.y-t.y)}function equals(e,t){return e.x===t.x&&e.y===t.y}function intersects(e,t,i,n){return!!(equals(e,t)&&equals(i,n)||equals(e,n)&&equals(i,t))||area(e,t,i)>0!=area(e,t,n)>0&&area(i,n,e)>0!=area(i,n,t)>0}function intersectsPolygon(e,t){var i=e;do{if(i.i!==e.i&&i.next.i!==e.i&&i.i!==t.i&&i.next.i!==t.i&&intersects(i,i.next,e,t))return!0;i=i.next}while(i!==e);return!1}function locallyInside(e,t){return area(e.prev,e,e.next)<0?area(e,t,e.next)>=0&&area(e,e.prev,t)>=0:area(e,t,e.prev)<0||area(e,e.next,t)<0}function middleInside(e,t){var i=e,n=!1,r=(e.x+t.x)/2,s=(e.y+t.y)/2;do{i.y>s!=i.next.y>s&&i.next.y!==i.y&&r<(i.next.x-i.x)*(s-i.y)/(i.next.y-i.y)+i.x&&(n=!n),i=i.next}while(i!==e);return n}function splitPolygon(e,t){var i=new Node(e.i,e.x,e.y),n=new Node(t.i,t.x,t.y),r=e.next,s=t.prev;return e.next=t,t.prev=e,i.next=r,r.prev=i,n.next=i,i.prev=n,s.next=n,n.prev=s,n}function insertNode(e,t,i,n){var r=new Node(e,t,i);return n?(r.next=n.next,r.prev=n,n.next.prev=r,n.next=r):(r.prev=r,r.next=r),r}function removeNode(e){e.next.prev=e.prev,e.prev.next=e.next,e.prevZ&&(e.prevZ.nextZ=e.nextZ),e.nextZ&&(e.nextZ.prevZ=e.prevZ)}function Node(e,t,i){this.i=e,this.x=t,this.y=i,this.prev=null,this.next=null,this.z=null,this.prevZ=null,this.nextZ=null,this.steiner=!1}function signedArea(e,t,i,n){for(var r=0,s=t,o=i-n;s0&&(n+=e[r-1].length,i.holes.push(n))}return i},earcut_1.default=default_1,me.Polygon=me.Object.extend({init:function(e,t,i){this.pos=new me.Vector2d,this._bounds=void 0,this.points=null,this.edges=[],this.indices=[],this.normals=[],this.shapeType="Polygon",this.setShape(e,t,i)},onResetEvent:function(e,t,i){this.setShape(e,t,i)},setShape:function(e,t,i){if(this.pos.set(e,t),!Array.isArray(i))return this;if(i[0]instanceof me.Vector2d)this.points=i;else{var n=this.points=[];i.forEach(function(e){n.push(new me.Vector2d(e.x,e.y))})}return this.recalc(),this.updateBounds(),this},transform:function(e){for(var t=this.points,i=t.length,n=0;nt!=c>t&&e<(d-u)*(t-l)/(c-l)+u&&(i=!i)}return i},getBounds:function(){return this._bounds},updateBounds:function(){return this._bounds||(this._bounds=new me.Rect(0,0,0,0)),this._bounds.setPoints(this.points),this._bounds.translateV(this.pos),this._bounds},clone:function(){var e=[];return this.points.forEach(function(t){e.push(t.clone())}),new me.Polygon(this.pos.x,this.pos.y,e)}}),me.Rect=me.Polygon.extend({init:function(e,t,i,n){this._super(me.Polygon,"init",[e,t,[new me.Vector2d(0,0),new me.Vector2d(i,0),new me.Vector2d(i,n),new me.Vector2d(0,n)]]),this.shapeType="Rectangle"},onResetEvent:function(e,t,i,n){this.setShape(e,t,i,n)},setShape:function(e,t,i,n){var r=i;return 4===arguments.length&&((r=this.points)[0].set(0,0),r[1].set(i,0),r[2].set(i,n),r[3].set(0,n)),this._super(me.Polygon,"setShape",[e,t,r]),this._width=this.points[2].x,this._height=this.points[2].y,this},resize:function(e,t){return this.width=e,this.height=t,this},getBounds:function(){return this},setPoints:function(e){var t=1/0,i=1/0,n=-1/0,r=-1/0;return e.forEach(function(e){t=Math.min(t,e.x),i=Math.min(i,e.y),n=Math.max(n,e.x),r=Math.max(r,e.y)}),this.setShape(t,i,n-t,r-i),this},recalc:function(){return this._super(me.Polygon,"recalc"),this._width=this.points[2].x,this._height=this.points[2].y,this},updateBounds:function(){return this},clone:function(){return new me.Rect(this.pos.x,this.pos.y,this._width,this._height)},copy:function(e){return this.setShape(e.pos.x,e.pos.y,e._width,e._height)},translate:function(e,t){return this.pos.x+=e,this.pos.y+=t,this},translateV:function(e){return this.translate(e.x,e.y)},union:function(e){var t=Math.min(this.left,e.left),i=Math.min(this.top,e.top);return this.resize(Math.max(this.right,e.right)-t,Math.max(this.bottom,e.bottom)-i),this.pos.set(t,i),this},overlaps:function(e){return this.left=this.left&&e.right<=this.right&&e.top>=this.top&&e.bottom<=this.bottom},containsPoint:function(e,t){return e>=this.left&&e<=this.right&&t>=this.top&&t<=this.bottom},equals:function(e){return e.left===this.left&&e.right===this.right&&e.top===this.top&&e.bottom===this.bottom},isFinite:function(e){function t(){return e.apply(this,arguments)}return t.toString=function(){return e.toString()},t}(function(){return isFinite(this.pos.x)&&isFinite(this.pos.y)&&isFinite(this._width)&&isFinite(this._height)}),toPolygon:function(){return new me.Polygon(this.pos.x,this.pos.y,this.points)}}),Object.defineProperty(me.Rect.prototype,"left",{get:function(){return this.pos.x},configurable:!0}),Object.defineProperty(me.Rect.prototype,"right",{get:function(){var e=this._width;return this.pos.x+e||e},configurable:!0}),Object.defineProperty(me.Rect.prototype,"top",{get:function(){return this.pos.y},configurable:!0}),Object.defineProperty(me.Rect.prototype,"bottom",{get:function(){var e=this._height;return this.pos.y+e||e},configurable:!0}),Object.defineProperty(me.Rect.prototype,"width",{get:function(){return this._width},set:function(e){this._width!==e&&(this.points[1].x=this.points[2].x=e,this.recalc())},configurable:!0}),Object.defineProperty(me.Rect.prototype,"height",{get:function(){return this._height},set:function(e){this._height!==e&&(this.points[2].y=this.points[3].y=e,this.recalc())},configurable:!0}),Object.defineProperty(me.Rect.prototype,"centerX",{get:function(){return this.pos.x+this._width/2},set:function(e){this.pos.x=e-this._width/2},configurable:!0}),Object.defineProperty(me.Rect.prototype,"centerY",{get:function(){return this.pos.y+this._height/2},set:function(e){this.pos.y=e-this._height/2},configurable:!0}),me.Line=me.Polygon.extend({containsPointV:function(e){return this.containsPoint(e.x,e.y)},containsPoint:function(e,t){e-=this.pos.x,t-=this.pos.y;var i=this.points[0],n=this.points[1];return(t-i.y)*(n.x-i.x)==(n.y-i.y)*(e-i.x)},recalc:function(){var e=this.edges,t=this.normals,i=this.points;if(2!==i.length)throw new Error("Requires exactly 2 points");return void 0===e[0]&&(e[0]=new me.Vector2d),e[0].copy(i[1]).sub(i[0]),void 0===t[0]&&(t[0]=new me.Vector2d),t[0].copy(e[0]).perp().normalize(),this},clone:function(){var e=[];return this.points.forEach(function(t){e.push(t.clone())}),new me.Line(this.pos.x,this.pos.y,e)}}),me.Body=me.Rect.extend({init:function(e,t,i){if(this.ancestor=e,void 0===this.shapes&&(this.shapes=[]),this.collisionMask=me.collision.types.ALL_OBJECT,this.collisionType=me.collision.types.ENEMY_OBJECT,void 0===this.vel&&(this.vel=new me.Vector2d),this.vel.set(0,0),void 0===this.accel&&(this.accel=new me.Vector2d),this.accel.set(0,0),void 0===this.force&&(this.force=new me.Vector2d),this.force.set(0,0),void 0===this.friction&&(this.friction=new me.Vector2d),this.friction.set(0,0),this.bounce=0,this.mass=1,void 0===this.maxVel&&(this.maxVel=new me.Vector2d),this.maxVel.set(490,490),void 0===this.gravity&&(this.gravity=new me.Vector2d),this.gravity.set(0,"number"==typeof me.sys.gravity?me.sys.gravity:.98),this.falling=!1,this.jumping=!1,this._super(me.Rect,"init",[0,0,this.ancestor.width,this.ancestor.height]),"function"==typeof i&&(this.onBodyUpdate=i),Array.isArray(t)){for(var n=0;n0&&(this.vel.x*=-this.bounce)),0!==t.y){this.vel.y=~~(.5+this.vel.y-t.y)||0,this.bounce>0&&(this.vel.y*=-this.bounce);var i=Math.sign(this.gravity.y)||1;this.falling=t.y>=i,this.jumping=t.y<=-i}},updateBounds:function(){if(this.shapes.length>0){var e=this.shapes[0].getBounds();this.pos.setV(e.pos),this.resize(e.width,e.height);for(var t=1;t0?n:0,e.y=s<0?s:o>0?o:0},computeVelocity:function(e){this.force.x&&(e.x+=this.force.x*me.timer.tick),this.force.y&&(e.y+=this.force.y*me.timer.tick),(this.friction.x||this.friction.y)&&this.applyFriction(e),this.gravity.y&&(e.x+=this.gravity.x*this.mass*me.timer.tick),this.gravity.y&&(e.y+=this.gravity.y*this.mass*me.timer.tick,this.falling=e.y*Math.sign(this.gravity.y)>0,this.jumping=!this.falling&&this.jumping),0!==e.y&&(e.y=me.Math.clamp(e.y,-this.maxVel.y,this.maxVel.y)),0!==e.x&&(e.x=me.Math.clamp(e.x,-this.maxVel.x,this.maxVel.x))},update:function(){return this.computeVelocity(this.vel),this.ancestor.pos.add(this.vel),0!==this.vel.x||0!==this.vel.y},destroy:function(){this.onBodyUpdate=void 0,this.ancestor=void 0,this.shapes.length=0}}),function(){var e=[],t=function(t,i,n,r){if(e.length>0){var s=e.pop();return s.bounds=t,s.max_objects=i||4,s.max_levels=n||4,s.level=r||0,s}return new me.QuadTree(t,i,n,r)},i=new me.Vector2d;function n(e,t,i,n){this.max_objects=t||4,this.max_levels=i||4,this.level=n||0,this.bounds=e,this.objects=[],this.nodes=[]}n.prototype.split=function(){var e=this.level+1,i=~~(.5+this.bounds.width/2),n=~~(.5+this.bounds.height/2),r=~~(.5+this.bounds.pos.x),s=~~(.5+this.bounds.pos.y);this.nodes[0]=t({pos:{x:r+i,y:s},width:i,height:n},this.max_objects,this.max_levels,e),this.nodes[1]=t({pos:{x:r,y:s},width:i,height:n},this.max_objects,this.max_levels,e),this.nodes[2]=t({pos:{x:r,y:s+n},width:i,height:n},this.max_objects,this.max_levels,e),this.nodes[3]=t({pos:{x:r+i,y:s+n},width:i,height:n},this.max_objects,this.max_levels,e)},n.prototype.getIndex=function(e){var t=e.getBounds(),n=t.pos;(e.floating||e.ancestor&&e.ancestor.floating)&&(n=me.game.viewport.localToWorld(n.x,n.y,i));var r=-1,s=n.x,o=n.y,a=t.width,h=t.height,l=this.bounds.pos.x+this.bounds.width/2,u=this.bounds.pos.y+this.bounds.height/2,c=ou;return sl&&(c?r=0:d&&(r=3)),r},n.prototype.insertContainer=function(e){for(var t,i=e.children.length;i--,t=e.children[i];)!0!==t.isKinematic&&(t instanceof me.Container?("rootContainer"!==t.name&&this.insert(t),this.insertContainer(t)):"function"==typeof t.getBounds&&this.insert(t))},n.prototype.insert=function(e){var t=-1;if(this.nodes.length>0&&-1!==(t=this.getIndex(e)))this.nodes[t].insert(e);else if(this.objects.push(e),this.objects.length>this.max_objects&&this.level0){var n=this.getIndex(e);if(-1!==n)i=i.concat(this.nodes[n].retrieve(e));else for(var r=0;r0){var i=this.getIndex(e);-1!==i&&(t=me.utils.array.remove(this.nodes[i],e))&&this.nodes[i].isPrunable()&&this.nodes.splice(i,1)}return!1===t&&-1!==this.objects.indexOf(e)&&(me.utils.array.remove(this.objects,e),t=!0),t},n.prototype.isPrunable=function(){return!(this.hasChildren()||this.objects.length>0)},n.prototype.hasChildren=function(){for(var e=0;e0||t.objects.length>0)return!0}return!1},n.prototype.clear=function(t){this.objects.length=0;for(var i=0;ir&&(r=a)}i[0]=n,i[1]=r}function u(e,t,i,r,o,a){var h=s.pop(),u=s.pop(),c=n.pop().copy(t).sub(e),d=c.dotProduct(o);if(l(i,o,h),l(r,o,u),u[0]+=d,u[1]+=d,h[0]>u[1]||u[0]>h[1])return n.push(c),s.push(h),s.push(u),!0;if(a){var f=0;if(h[0]u[1])f=h[0]-u[1],a.aInB=!1;else{var g=h[1]-u[0],v=u[1]-h[0];f=gs?i:t}me.collision=((a={quadTree:null,maxDepth:4,maxChildren:8,bounds:null,types:{NO_OBJECT:0,PLAYER_OBJECT:1,NPC_OBJECT:2,ENEMY_OBJECT:4,COLLECTABLE_OBJECT:8,ACTION_OBJECT:16,PROJECTILE_OBJECT:32,WORLD_SHAPE:64,USER:128,ALL_OBJECT:4294967295},init:function(){a.bounds=me.game.world.getBounds().clone(),a.quadTree=new me.QuadTree(a.bounds,a.maxChildren,a.maxDepth),me.event.subscribe(me.event.LEVEL_LOADED,function(){a.bounds.copy(me.game.world.getBounds()),a.quadTree.clear(a.bounds)})},ResponseObject:function(){this.a=null,this.b=null,this.overlapN=new me.Vector2d,this.overlapV=new me.Vector2d,this.aInB=!0,this.bInA=!0,this.indexShapeA=-1,this.indexShapeB=-1,this.overlap=Number.MAX_VALUE}}).ResponseObject.prototype.clear=function(){return this.aInB=!0,this.bInA=!0,this.overlap=Number.MAX_VALUE,this.indexShapeA=-1,this.indexShapeB=-1,this},a.response=new a.ResponseObject,a.shouldCollide=function(e,t){return!0!==e.isKinematic&&!0!==t.isKinematic&&e.body&&t.body&&0!=(e.body.collisionMask&t.body.collisionType)&&0!=(e.body.collisionType&t.body.collisionMask)},a.check=function(e,t){for(var i,n=0,r=t||a.response,s=a.quadTree.retrieve(e),o=s.length;i=s[--o];)if(i!==e&&a.shouldCollide(e,i)&&e.getBounds().overlaps(i.getBounds())){var h=e.body.shapes.length,l=i.body.shapes.length;if(0===h||0===l)continue;var u=0;do{var c=e.body.getShape(u),d=0;do{var f=i.body.getShape(d);!0===a["test"+c.shapeType+f.shapeType].call(this,e,c,i,f,r.clear())&&(n++,r.indexShapeA=u,r.indexShapeB=d,e.onCollision&&!1!==e.onCollision(r,i)&&e.body.respondToCollision.call(e.body,r),i.onCollision&&!1!==i.onCollision(r,e)&&i.body.respondToCollision.call(i.body,r)),d++}while(d0},a.rayCast=function(e,t){for(var i,n=0,r=t||[],s=a.quadTree.retrieve(e.getBounds()),o=s.length;i=s[--o];)if(i.body&&e.getBounds().overlaps(i.getBounds())){var l=i.body.shapes.length;if(0===i.body.shapes.length)continue;var u=e,c=0;do{var d=i.body.getShape(c);a["test"+u.shapeType+d.shapeType].call(this,h,u,i,d)&&(r[n]=i,n++),c++}while(cu)return n.push(o),!1;if(s){var d=Math.sqrt(c);s.a=e,s.b=i,s.overlap=l-d,s.overlapN.copy(o.normalize()),s.overlapV.copy(o).scale(s.overlap),s.aInB=a<=h&&d<=h-a,s.bInA=h<=a&&d<=a-h}return n.push(o),!0},a.testPolygonEllipse=function(t,r,s,o,a){for(var h=n.pop().copy(s.pos).add(s.ancestor._absPos).add(o.pos).sub(t.pos).add(t.ancestor._absPos).sub(r.pos),l=o.radius,u=l*l,d=r.points,f=r.edges,p=f.length,m=n.pop(),g=n.pop(),v=n.pop(),y=0,_=0;_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)0){var i=this.getAnimationFrameObjectByIndex(this.current.idx).delay;for(this.dt+=e;this.dt>=i;){t=!0,this.dt-=i;var n=this.current.length>1?this.current.idx+1:this.current.idx;if(this.setAnimationFrame(n),0===this.current.idx&&"function"==typeof this.resetAnim&&!1===this.resetAnim()){this.setAnimationFrame(this.current.length-1),this.dt%=i;break}i=this.getAnimationFrameObjectByIndex(this.current.idx).delay}}return this._flicker.isFlickering&&(this._flicker.duration-=e,this._flicker.duration<0&&("function"==typeof this._flicker.callback&&this._flicker.callback(),this.flicker(-1)),t=!0),t},updateBoundsPos:function(e,t){var i=this.getBounds();return i.pos.set(e-this.anchorPoint.x*i.width,t-this.anchorPoint.y*i.height),this.ancestor instanceof me.Container&&!this.floating&&i.pos.add(this.ancestor._absPos),i},onAnchorUpdate:function(e,t){this.anchorPoint.setMuted(e,t),this.updateBoundsPos(this.pos.x,this.pos.y)},destroy:function(){me.pool.push(this.offset),this.offset=void 0,this._super(me.Renderable,"destroy")},draw:function(e){if(!this._flicker.isFlickering||(this._flicker.state=!this._flicker.state,this._flicker.state)){var t=this.current,i=this.pos.x,n=this.pos.y,r=t.width,s=t.height,o=t.offset,a=this.offset;0!==t.angle&&(e.translate(-i,-n),e.rotate(t.angle),i-=s,r=t.height,s=t.width),e.drawImage(this.image,a.x+o.x,a.y+o.y,r,s,i,n,r,s)}}}),me.GUI_Object=me.Sprite.extend({init:function(e,t,i){this.isClickable=!0,this.holdThreshold=250,this.isHoldable=!1,this.hover=!1,this.holdTimeout=null,this.updated=!1,this.released=!0,this._super(me.Sprite,"init",[e,t,i]),this.floating=!0,this.isKinematic=!1},update:function(e){var t=this._super(me.Sprite,"update",[e]);return this.updated?(this.released||(this.updated=!1),!0):t},clicked:function(e){if(0===e.button&&this.isClickable)return this.updated=!0,this.released=!1,this.isHoldable&&(null!==this.holdTimeout&&me.timer.clearTimeout(this.holdTimeout),this.holdTimeout=me.timer.setTimeout(this.hold.bind(this),this.holdThreshold,!1),this.released=!1),this.onClick.call(this,e)},onClick:function(){return!1},enter:function(e){return this.hover=!0,this.onOver.call(this,e)},onOver:function(){},leave:function(e){return this.hover=!1,this.release.call(this,e),this.onOut.call(this,e)},onOut:function(){},release:function(e){if(!1===this.released)return this.released=!0,me.timer.clearTimeout(this.holdTimeout),this.onRelease.call(this,e)},onRelease:function(){return!1},hold:function(){me.timer.clearTimeout(this.holdTimeout),this.released||this.onHold.call(this)},onHold:function(){},onActivateEvent:function(){me.input.registerPointerEvent("pointerdown",this,this.clicked.bind(this)),me.input.registerPointerEvent("pointerup",this,this.release.bind(this)),me.input.registerPointerEvent("pointercancel",this,this.release.bind(this)),me.input.registerPointerEvent("pointerenter",this,this.enter.bind(this)),me.input.registerPointerEvent("pointerleave",this,this.leave.bind(this))},onDeactivateEvent:function(){me.input.releasePointerEvent("pointerdown",this),me.input.releasePointerEvent("pointerup",this),me.input.releasePointerEvent("pointercancel",this),me.input.releasePointerEvent("pointerenter",this),me.input.releasePointerEvent("pointerleave",this),me.timer.clearTimeout(this.holdTimeout)}}),deferredRemove=function(e,t){this.removeChildNow(e,t)},globalFloatingCounter=0,me.Container=me.Renderable.extend({init:function(e,t,i,n,r){this.pendingSort=null,this._super(me.Renderable,"init",[e||0,t||0,i||1/0,n||1/0]),this.root=r||!1,this.children=[],this.sortOn=me.game.sortOn,this.autoSort=!0,this.autoDepth=!0,this.clipping=!1,this.onChildChange=function(){},this.drawCount=0,this.childBounds=this.getBounds().clone(),this.autoTransform=!0,this.isKinematic=!1,!0===this.root&&me.event.subscribe(me.event.CANVAS_ONRESIZE,this.updateChildBounds.bind(this))},reset:function(){this.pendingSort&&(clearTimeout(this.pendingSort),this.pendingSort=null);for(var e,t=this.children.length;t>=0;e=this.children[--t])e&&!e.isPersistent&&this.removeChildNow(e);void 0!==this.currentTransform&&this.currentTransform.identity()},addChild:function(e,t){return e.ancestor instanceof me.Container?e.ancestor.removeChildNow(e):e.isRenderable&&(e.GUID=me.utils.createGUID(e.id)),e.ancestor=this,this.children.push(e),void 0!==e.pos&&("number"==typeof t?e.pos.z=t:!0===this.autoDepth&&(e.pos.z=this.children.length)),!0===this.autoSort&&this.sort(),"function"==typeof e.onActivateEvent&&this.isAttachedToRoot()&&e.onActivateEvent(),this.onChildChange.call(this,this.children.length-1),e},addChildAt:function(e,t){if(t>=0&&t1&&(i=t);n=0&&e=0&&t=0;r--){var s=this.children[r];n(s,e),s instanceof me.Container&&(i=i.concat(s.getChildByProp(e,t)))}return i},getChildByType:function(e){for(var t=[],i=this.children.length-1;i>=0;i--){var n=this.children[i];n instanceof e&&t.push(n),n instanceof me.Container&&(t=t.concat(n.getChildByType(e)))}return t},getChildByName:function(e){return this.getChildByProp("name",e)},getChildByGUID:function(e){var t=this.getChildByProp("GUID",e);return t.length>0?t[0]:null},updateChildBounds:function(){var e;this.childBounds.pos.set(1/0,1/0),this.childBounds.resize(-1/0,-1/0);for(var t,i=this.children.length;i--,t=this.children[i];)t.isRenderable&&null!==(e=t instanceof me.Container?t.childBounds:t.getBounds())&&this.childBounds.union(e);return this.childBounds},isAttachedToRoot:function(){if(!0===this.root)return!0;for(var e=this.ancestor;e;){if(!0===e.root)return!0;e=e.ancestor}return!1},updateBoundsPos:function(e,t){this._super(me.Renderable,"updateBoundsPos",[e,t]),this._absPos.set(e,t),this.ancestor instanceof me.Container&&!this.floating&&this._absPos.add(this.ancestor._absPos);for(var i,n=this.children.length;n--,i=this.children[n];)i.isRenderable&&i.updateBoundsPos(i.pos.x,i.pos.y);return this.getBounds()},onActivateEvent:function(){for(var e,t=this.children.length;t--,e=this.children[t];)"function"==typeof e.onActivateEvent&&e.onActivateEvent()},removeChild:function(e,t){if(!this.hasChild(e))throw new Error("Child is not mine.");me.utils.function.defer(deferredRemove,this,e,t)},removeChildNow:function(e,t){if(this.hasChild(e)&&this.getChildIndex(e)>=0){"function"==typeof e.onDeactivateEvent&&e.onDeactivateEvent(),t||("function"==typeof e.destroy&&e.destroy(),me.pool.push(e));var i=this.getChildIndex(e);i>=0&&(this.children.splice(i,1),e.ancestor=void 0),this.onChildChange.call(this,i)}},setChildsProperty:function(e,t,i){for(var n=this.children.length;n>=0;n--){var r=this.children[n];!0===i&&r instanceof me.Container&&r.setChildsProperty(e,t,i),r[e]=t}},moveUp:function(e){var t=this.getChildIndex(e);t-1>=0&&this.swapChildren(e,this.getChildAt(t-1))},moveDown:function(e){var t=this.getChildIndex(e);t>=0&&t+10&&(this.children.splice(0,0,this.children.splice(t,1)[0]),e.pos.z=this.children[1].pos.z+1)},moveToBottom:function(e){var t=this.getChildIndex(e);t>=0&&t0||r.floating)&&globalFloatingCounter++,r.inViewport=!1,me.state.current().cameras.forEach(function(e){e.isVisible(r,i)&&(r.inViewport=!0)}),t=(r.inViewport||r.alwaysUpdate)&&r.update(e)||t,r._absPos.setV(this._absPos).add(r.pos),globalFloatingCounter>0&&globalFloatingCounter--):t=r.update(e)||t);return t},draw:function(e,t){var i=!1;this.drawCount=0,!1===this.root&&!0===this.clipping&&!0===this.childBounds.isFinite()&&e.clipRect(this.childBounds.pos.x,this.childBounds.pos.y,this.childBounds.width,this.childBounds.height),e.translate(this.pos.x,this.pos.y);for(var n,r=this.children.length;r--,n=this.children[r];)n.isRenderable&&(i=!0===n.floating,(n.inViewport||i)&&(i&&(e.save(),e.resetTransform()),n.preDraw(e),n.draw(e,t),n.postDraw(e),i&&e.restore(),this.drawCount++))}}),MIN=Math.min,MAX=Math.max,targetV=new me.Vector2d,me.Camera2d=me.Renderable.extend({init:function(e,t,i,n){this._super(me.Renderable,"init",[e,t,i-e,n-t]),this.AXIS={NONE:0,HORIZONTAL:1,VERTICAL:2,BOTH:3},this.bounds=new me.Rect(-1/0,-1/0,1/0,1/0),this.smoothFollow=!0,this.damping=1,this.offset=new me.Vector2d,this.target=null,this.follow_axis=this.AXIS.NONE,this._shake={intensity:0,duration:0,axis:this.AXIS.BOTH,onComplete:null},this._fadeOut={color:null,tween:null},this._fadeIn={color:null,tween:null},this.name="default",this.setDeadzone(this.width/6,this.height/6),this.anchorPoint.set(0,0),this.isKinematic=!1,me.event.subscribe(me.event.GAME_RESET,this.reset.bind(this)),me.event.subscribe(me.event.CANVAS_ONRESIZE,this.resize.bind(this))},_followH:function(e){var t=this.pos.x;return e.x-this.pos.x>this.deadzone.right?t=MIN(e.x-this.deadzone.right,this.bounds.width-this.width):e.x-this.pos.xthis.deadzone.bottom?t=MIN(e.y-this.deadzone.bottom,this.bounds.height-this.height):e.y-this.pos.y0&&(this._shake.duration-=e,this._shake.duration<=0?(this._shake.duration=0,this.offset.setZero(),"function"==typeof this._shake.onComplete&&this._shake.onComplete()):(this._shake.axis!==this.AXIS.BOTH&&this._shake.axis!==this.AXIS.HORIZONTAL||(this.offset.x=(Math.random()-.5)*this._shake.intensity),this._shake.axis!==this.AXIS.BOTH&&this._shake.axis!==this.AXIS.VERTICAL||(this.offset.y=(Math.random()-.5)*this._shake.intensity)),t=!0),!0===t&&me.event.publish(me.event.VIEWPORT_ONCHANGE,[this.pos]),null==this._fadeIn.tween&&null==this._fadeOut.tween||(t=!0),t},shake:function(e,t,i,n,r){0!==this._shake.duration&&!0!==r||(this._shake.intensity=e,this._shake.duration=t,this._shake.axis=i||this.AXIS.BOTH,this._shake.onComplete="function"==typeof n?n:void 0)},fadeOut:function(e,t,i){this._fadeOut.color=me.pool.pull("me.Color").copy(e),this._fadeOut.tween=me.pool.pull("me.Tween",this._fadeOut.color).to({alpha:0},t||1e3).onComplete(i||null),this._fadeOut.tween.isPersistent=!0,this._fadeOut.tween.start()},fadeIn:function(e,t,i){this._fadeIn.color=me.pool.pull("me.Color").copy(e);var n=this._fadeIn.color.alpha;this._fadeIn.color.alpha=0,this._fadeIn.tween=me.pool.pull("me.Tween",this._fadeIn.color).to({alpha:n},t||1e3).onComplete(i||null),this._fadeIn.tween.isPersistent=!0,this._fadeIn.tween.start()},getWidth:function(){return this.width},getHeight:function(){return this.height},focusOn:function(e){var t=e.getBounds();this.moveTo(e.pos.x+t.pos.x+t.width/2,e.pos.y+t.pos.y+t.height/2)},isVisible:function(e,t){return!0===t||!0===e.floating?me.video.renderer.overlaps(e.getBounds()):e.getBounds().overlaps(this)},localToWorld:function(e,t,i){return(i=i||new me.Vector2d).set(e,t).add(this.pos).sub(me.game.world.pos),this.currentTransform.isIdentity()||this.currentTransform.multiplyVectorInverse(i),i},worldToLocal:function(e,t,i){return(i=i||new me.Vector2d).set(e,t),this.currentTransform.isIdentity()||this.currentTransform.multiplyVector(i),i.sub(this.pos).add(me.game.world.pos)},drawFX:function(e){this._fadeIn.tween&&(e.clearColor(this._fadeIn.color),1===this._fadeIn.color.alpha&&(this._fadeIn.tween=null,me.pool.push(this._fadeIn.color),this._fadeIn.color=null)),this._fadeOut.tween&&(e.clearColor(this._fadeOut.color),0===this._fadeOut.color.alpha&&(this._fadeOut.tween=null,me.pool.push(this._fadeOut.color),this._fadeOut.color=null))},draw:function(e,t){var i=this.pos.x+this.offset.x,n=this.pos.y+this.offset.y;t.currentTransform.translate(-i,-n),e.clipRect(0,0,this.width,this.height),this.preDraw(e),t.preDraw(e),t.draw(e,this),this.drawFX(e),t.postDraw(e),this.postDraw(e),t.currentTransform.translate(i,n)}}),me.Entity=me.Renderable.extend({init:function(e,t,i){if(this.children=[],"number"!=typeof i.width||"number"!=typeof i.height)throw new Error("height and width properties are mandatory when passing settings parameters to an object entity");this._super(me.Renderable,"init",[e,t,i.width,i.height]),i.image&&(i.framewidth=i.framewidth||i.width,i.frameheight=i.frameheight||i.height,this.renderable=new me.Sprite(0,0,i)),i.anchorPoint&&this.anchorPoint.set(i.anchorPoint.x,i.anchorPoint.y),"string"==typeof i.name&&(this.name=i.name),this.type=i.type||"",this.id=i.id||"",this.alive=!0;var n=Array.isArray(i.shapes)?i.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(void 0!==this.body?this.body.init(this,n,this.onBodyUpdate.bind(this)):this.body=new me.Body(this,n,this.onBodyUpdate.bind(this)),0===this.width&&0===this.height&&this.resize(this.body.width,this.body.height),void 0!==i.collisionMask&&this.body.setCollisionMask(i.collisionMask),void 0!==i.collisionType){if(void 0===me.collision.types[i.collisionType])throw new Error("Invalid value for the collisionType property");this.body.collisionType=me.collision.types[i.collisionType]}this.autoTransform=!1,this.isKinematic=!1},distanceTo:function(e){var t=this.getBounds(),i=e.getBounds(),n=t.pos.x+t.width/2-(i.pos.x+i.width/2),r=t.pos.y+t.height/2-(i.pos.y+i.height/2);return Math.sqrt(n*n+r*r)},distanceToPoint:function(e){var t=this.getBounds(),i=t.pos.x+t.width/2-e.x,n=t.pos.y+t.height/2-e.y;return Math.sqrt(i*i+n*n)},angleTo:function(e){var t=this.getBounds(),i=e.getBounds(),n=i.pos.x+i.width/2-(t.pos.x+t.width/2),r=i.pos.y+i.height/2-(t.pos.y+t.height/2);return Math.atan2(r,n)},angleToPoint:function(e){var t=this.getBounds(),i=e.x-(t.pos.x+t.width/2),n=e.y-(t.pos.y+t.height/2);return Math.atan2(n,i)},update:function(e){return this.renderable?this.renderable.update(e):this._super(me.Renderable,"update",[e])},updateBoundsPos:function(e,t){if(void 0!==this.body){var i=this.body.pos;this._super(me.Renderable,"updateBoundsPos",[e+i.x,t+i.y])}else this._super(me.Renderable,"updateBoundsPos",[e,t]);return this.getBounds()},onBodyUpdate:function(e){this.getBounds().resize(e.width,e.height),this.updateBoundsPos(this.pos.x,this.pos.y)},preDraw:function(e){e.save(),e.translate(this.pos.x+this.body.pos.x,this.pos.y+this.body.pos.y),this.renderable instanceof me.Renderable&&e.translate(this.anchorPoint.x*this.body.width,this.anchorPoint.y*this.body.height)},draw:function(e,t){var i=this.renderable;i instanceof me.Renderable&&(i.preDraw(e),i.draw(e,t),i.postDraw(e))},destroy:function(){this.renderable&&(this.renderable.destroy.apply(this.renderable,arguments),this.children.splice(0,1)),this._super(me.Renderable,"destroy",arguments)},onDeactivateEvent:function(){this.renderable&&this.renderable.onDeactivateEvent&&this.renderable.onDeactivateEvent()},onCollision:function(){return!1}}),Object.defineProperty(me.Entity.prototype,"renderable",{get:function(){return this.children[0]},set:function(e){if(!(e instanceof me.Renderable))throw new Error(e+"should extend me.Renderable");this.children[0]=e,this.children[0].ancestor=this},configurable:!0}),default_settings={cameras:[]},me.Stage=me.Object.extend({init:function(e){this.cameras=new Map,this.settings=Object.assign(default_settings,e||{})},reset:function(){var e=this;if(this.settings.cameras.forEach(function(t){e.cameras.set(t.name,t)}),!1===this.cameras.has("default")){if(void 0===default_camera){var t=me.video.renderer.getWidth(),i=me.video.renderer.getHeight();default_camera=new me.Camera2d(0,0,t,i)}this.cameras.set("default",default_camera)}me.game.reset(),this.onResetEvent.apply(this,arguments)},destroy:function(){this.cameras.clear(),this.onDestroyEvent.apply(this,arguments)},onResetEvent:function(){},onDestroyEvent:function(){}}),me.state=function(){var e={},t=-1,i=-1,n=!1,r={},s={color:"",duration:0},o=null,a=null,h=0;function l(){-1===i&&-1!==t&&(me.timer.reset(),i=window.requestAnimationFrame(u))}function u(e){var n=r[t].stage;me.game.update(e,n),me.game.draw(n),-1!==i&&(i=window.requestAnimationFrame(u))}function c(){window.cancelAnimationFrame(i),i=-1}function d(e){c(),r[t]&&r[t].stage.destroy(),r[e]&&(r[t=e].stage.reset.apply(r[t].stage,a),l(),o&&o(),me.game.repaint())}return e.LOADING=0,e.MENU=1,e.READY=2,e.PLAY=3,e.GAMEOVER=4,e.GAME_END=5,e.SCORE=6,e.CREDITS=7,e.SETTINGS=8,e.USER=100,e.onPause=null,e.onResume=null,e.onStop=null,e.onRestart=null,e.init=function(){e.set(e.LOADING,new me.DefaultLoadingScreen)},e.stop=function(i){t!==e.LOADING&&e.isRunning()&&(c(),!0===i&&me.audio.pauseTrack(),h=window.performance.now(),me.event.publish(me.event.STATE_STOP),"function"==typeof e.onStop&&e.onStop())},e.pause=function(i){t===e.LOADING||e.isPaused()||(n=!0,!0===i&&me.audio.pauseTrack(),h=window.performance.now(),me.event.publish(me.event.STATE_PAUSE),"function"==typeof e.onPause&&e.onPause())},e.restart=function(t){e.isRunning()||(l(),!0===t&&me.audio.resumeTrack(),h=window.performance.now()-h,me.game.repaint(),me.event.publish(me.event.STATE_RESTART,[h]),"function"==typeof e.onRestart&&e.onRestart())},e.resume=function(i){e.isPaused()&&(n&&-1!==t&&(me.timer.reset(),n=!1),!0===i&&me.audio.resumeTrack(),h=window.performance.now()-h,me.event.publish(me.event.STATE_RESUME,[h]),"function"==typeof e.onResume&&e.onResume())},e.isRunning=function(){return-1!==i},e.isPaused=function(){return n},e.set=function(e,t){if(!(t instanceof me.Stage))throw new Error(t+" is not an instance of me.Stage");r[e]={},r[e].stage=t,r[e].transition=!0},e.current=function(){return r[t].stage},e.transition=function(e,t,i){"fade"===e&&(s.color=t,s.duration=i)},e.setTransition=function(e,t){r[e].transition=t},e.change=function(t){if(void 0===r[t])throw new Error("Undefined Stage for state '"+t+"'");e.isCurrent(t)||(a=null,arguments.length>1&&(a=Array.prototype.slice.call(arguments,1)),s.duration&&r[t].transition?(o=function(){me.game.viewport.fadeOut(s.color,s.duration)},me.game.viewport.fadeIn(s.color,s.duration,function(){me.utils.function.defer(d,this,t)})):me.utils.function.defer(d,this,t))},e.isCurrent=function(e){return t===e},e}(),ProgressBar=me.Renderable.extend({init:function(e,t,i,n){this._super(me.Renderable,"init",[e,t,i,n]),this.invalidate=!1,this.progress=0,this.anchorPoint.set(0,0)},onProgressUpdate:function(e){this.progress=~~(e*this.width),this.invalidate=!0},update:function(){return!0===this.invalidate&&(this.invalidate=!1,!0)},draw:function(e){var t=e.getColor(),i=e.getHeight();e.setColor("black"),e.fillRect(this.pos.x,i/2,this.width,this.height/2),e.setColor("#55aa00"),e.fillRect(this.pos.x,i/2,this.progress,this.height/2),e.setColor(t)}}),IconLogo=me.Renderable.extend({init:function(e,t){this._super(me.Renderable,"init",[e,t,100,85]),this.iconCanvas=me.video.createCanvas(me.Math.nextPowerOfTwo(this.width),me.Math.nextPowerOfTwo(this.height),!1);var i=me.video.renderer.getContext2d(this.iconCanvas);i.beginPath(),i.moveTo(.7,48.9),i.bezierCurveTo(10.8,68.9,38.4,75.8,62.2,64.5),i.bezierCurveTo(86.1,53.1,97.2,27.7,87,7.7),i.lineTo(87,7.7),i.bezierCurveTo(89.9,15.4,73.9,30.2,50.5,41.4),i.bezierCurveTo(27.1,52.5,5.2,55.8,.7,48.9),i.lineTo(.7,48.9),i.closePath(),i.fillStyle="rgb(255, 255, 255)",i.fill(),i.beginPath(),i.moveTo(84,7),i.bezierCurveTo(87.6,14.7,72.5,30.2,50.2,41.6),i.bezierCurveTo(27.9,53,6.9,55.9,3.2,48.2),i.bezierCurveTo(-.5,40.4,14.6,24.9,36.9,13.5),i.bezierCurveTo(59.2,2.2,80.3,-.8,84,7),i.lineTo(84,7),i.closePath(),i.lineWidth=5.3,i.strokeStyle="rgb(255, 255, 255)",i.lineJoin="miter",i.miterLimit=4,i.stroke(),this.anchorPoint.set(.5,.5)},draw:function(e){e.drawImage(this.iconCanvas,this.pos.x,this.pos.y)}}),TextLogo=me.Renderable.extend({init:function(e,t){this._super(me.Renderable,"init",[0,0,e,t]),this.fontCanvas=me.video.createCanvas(256,64),this.drawFont(me.video.renderer.getContext2d(this.fontCanvas)),this.anchorPoint.set(0,0)},drawFont:function(e){var t=me.pool.pull("me.Text",0,0,{font:"century gothic",size:32,fillStyle:"white",textAlign:"middle",textBaseline:"top",text:"melon"}),i=me.pool.pull("me.Text",0,0,{font:"century gothic",size:32,fillStyle:"#55aa00",textAlign:"middle",textBaseline:"top",bold:!0,text:"JS"}),n=t.measureText(e).width,r=i.measureText(e).width;this.pos.x=Math.round((this.width-n-r)/2),this.pos.y=Math.round(this.height/2+16),t._drawFont(e,"melon",0,0),i._drawFont(e,"JS",n,0),me.pool.push(t),me.pool.push(i)},draw:function(e){e.drawImage(this.fontCanvas,this.pos.x,this.pos.y)}}),me.DefaultLoadingScreen=me.Stage.extend({onResetEvent:function(){me.game.world.addChild(new me.ColorLayer("background","#202020",0),0);var e=new ProgressBar(0,me.video.renderer.getHeight()/2,me.video.renderer.getWidth(),8);this.loaderHdlr=me.event.subscribe(me.event.LOADER_PROGRESS,e.onProgressUpdate.bind(e)),this.resizeHdlr=me.event.subscribe(me.event.VIEWPORT_ONRESIZE,e.resize.bind(e)),me.game.world.addChild(e,1);var t=new IconLogo(me.video.renderer.getWidth()/2,me.video.renderer.getHeight()/2-e.height-35);me.game.world.addChild(t,1),me.game.world.addChild(new TextLogo(me.video.renderer.getWidth(),me.video.renderer.getHeight()),1)},onDestroyEvent:function(){me.event.unsubscribe(this.loaderHdlr),me.event.unsubscribe(this.resizeHdlr),this.loaderHdlr=this.resizeHdlr=null}}),me.loader=function(){var e={},t={},i={},n={},r={},s={},o=0,a=0,h=0;return e.nocache="",e.onload=void 0,e.onProgress=void 0,e.crossOrigin=void 0,e.withCredentials=!1,e.onResourceLoaded=function(t){var i=++a/o;e.onProgress&&e.onProgress(i,t),me.event.publish(me.event.LOADER_PROGRESS,[i,t])},e.onLoadingError=function(e){throw new Error("Failed loading resource "+e.src)},e.setNocache=function(t){e.nocache=t?"?"+~~(1e7*Math.random()):""},e.setBaseURL=function(e,t){"*"!==e?s[e]=t:(s.audio=t,s.binary=t,s.image=t,s.json=t,s.js=t,s.tmx=t,s.tsx=t)},e.preload=function(t,i,n){for(var r=0;r>>9];if(t)return t[511&e]||0}return 0},setKerning:function(e,t){this.kerning||(this.kerning={});var i=this.kerning[e>>>9];void 0===i&&(this.kerning[e>>>9]={},i=this.kerning[e>>>9]),i[511&e]=t}}),me.BitmapTextData=me.Object.extend({init:function(e){this.padTop=0,this.padRight=0,this.padBottom=0,this.padLeft=0,this.lineHeight=0,this.capHeight=1,this.descent=0,this.glyphs={},this.parse(e)},_createSpaceGlyph:function(){var e=" ".charCodeAt(0),t=this.glyphs[e];t||((t=new Glyph).id=e,t.xadvance=this._getFirstGlyph().xadvance,this.glyphs[e]=t)},_getFirstGlyph:function(){for(var e=Object.keys(this.glyphs),t=0;t32)return this.glyphs[e[t]];return null},_getValueFromPair:function(e,t){var i=e.match(t);if(!i)throw new Error("Could not find pattern "+t+" in string: "+e);return i[0].split("=")[1]},parse:function(e){if(!e)throw new Error("File containing font data was empty, cannot load the bitmap font.");var t=e.split(/\r\n|\n/),i=e.match(/padding\=\d+,\d+,\d+,\d+/g);if(!i)throw new Error("Padding not found in first line");var n=i[0].split("=")[1].split(",");this.padTop=parseFloat(n[0]),this.padLeft=parseFloat(n[1]),this.padBottom=parseFloat(n[2]),this.padRight=parseFloat(n[3]),this.lineHeight=parseFloat(this._getValueFromPair(t[1],/lineHeight\=\d+/g));for(var r=parseFloat(this._getValueFromPair(t[1],/base\=\d+/g)),s=this.padTop+this.padBottom,o=null,a=4;a0&&o.height>0&&(this.descent=Math.min(r+o.yoffset,this.descent)),this.glyphs[f]=o}}this.descent+=this.padBottom,this._createSpaceGlyph();var p=null;for(a=0;a=0&&e<=1){if(t._volume=e,t._muted)return t;t.usingWebAudio&&t.masterGain.gain.setValueAtTime(e,i.ctx.currentTime);for(var n=0;n=0;t--)e._howls[t].unload();return e.usingWebAudio&&e.ctx&&void 0!==e.ctx.close&&(e.ctx.close(),e.ctx=null,u()),e},codecs:function(e){return(this||i)._codecs[e.replace(/^x-/,"")]},_setup:function(){var e=this||i;if(e.state=e.ctx&&e.ctx.state||"suspended",e._autoSuspend(),!e.usingWebAudio)if("undefined"!=typeof Audio)try{void 0===(new Audio).oncanplaythrough&&(e._canPlayEvent="canplay")}catch(t){e.noAudio=!0}else e.noAudio=!0;try{(new Audio).muted&&(e.noAudio=!0)}catch(e){}return e.noAudio||e._setupCodecs(),e},_setupCodecs:function(){var e=this||i,t=null;try{t="undefined"!=typeof Audio?new Audio:null}catch(t){return e}if(!t||"function"!=typeof t.canPlayType)return e;var n=t.canPlayType("audio/mpeg;").replace(/^no$/,""),r=e._navigator&&e._navigator.userAgent.match(/OPR\/([0-6].)/g),s=r&&parseInt(r[0].split("/")[1],10)<33;return e._codecs={mp3:!(s||!n&&!t.canPlayType("audio/mp3;").replace(/^no$/,"")),mpeg:!!n,opus:!!t.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/,""),ogg:!!t.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),oga:!!t.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),wav:!!t.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),aac:!!t.canPlayType("audio/aac;").replace(/^no$/,""),caf:!!t.canPlayType("audio/x-caf;").replace(/^no$/,""),m4a:!!(t.canPlayType("audio/x-m4a;")||t.canPlayType("audio/m4a;")||t.canPlayType("audio/aac;")).replace(/^no$/,""),mp4:!!(t.canPlayType("audio/x-mp4;")||t.canPlayType("audio/mp4;")||t.canPlayType("audio/aac;")).replace(/^no$/,""),weba:!!t.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,""),webm:!!t.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,""),dolby:!!t.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/,""),flac:!!(t.canPlayType("audio/x-flac;")||t.canPlayType("audio/flac;")).replace(/^no$/,"")},e},_unlockAudio:function(){var e=this||i,t=/iPhone|iPad|iPod|Android|BlackBerry|BB10|Silk|Mobi|Chrome|Safari/i.test(e._navigator&&e._navigator.userAgent);if(!e._audioUnlocked&&e.ctx&&t){e._audioUnlocked=!1,e.autoUnlock=!1,e._mobileUnloaded||44100===e.ctx.sampleRate||(e._mobileUnloaded=!0,e.unload()),e._scratchBuffer=e.ctx.createBuffer(1,1,22050);var n=function(t){for(var i=0;i0?a._seek:n._sprite[e][0]/1e3),u=Math.max(0,(n._sprite[e][0]+n._sprite[e][1])/1e3-l),c=1e3*u/Math.abs(a._rate),d=n._sprite[e][0]/1e3,f=(n._sprite[e][0]+n._sprite[e][1])/1e3,p=!(!a._loop&&!n._sprite[e][2]);a._sprite=e,a._ended=!1;var m=function(){a._paused=!1,a._seek=l,a._start=d,a._stop=f,a._loop=p};if(!(l>=f)){var g=a._node;if(n._webAudio){var v=function(){n._playLock=!1,m(),n._refreshBuffer(a);var e=a._muted||n._muted?0:a._volume;g.gain.setValueAtTime(e,i.ctx.currentTime),a._playStart=i.ctx.currentTime,void 0===g.bufferSource.start?a._loop?g.bufferSource.noteGrainOn(0,l,86400):g.bufferSource.noteGrainOn(0,l,u):a._loop?g.bufferSource.start(0,l,86400):g.bufferSource.start(0,l,u),c!==1/0&&(n._endTimers[a._id]=setTimeout(n._ended.bind(n,a),c)),t||setTimeout(function(){n._emit("play",a._id),n._loadQueue()},0)};"running"===i.state?v():(n._playLock=!0,n.once("resume",v),n._clearTimer(a._id))}else{var y=function(){g.currentTime=l,g.muted=a._muted||n._muted||i._muted||g.muted,g.volume=a._volume*i.volume(),g.playbackRate=a._rate;try{var r=g.play();if(r&&"undefined"!=typeof Promise&&(r instanceof Promise||"function"==typeof r.then)?(n._playLock=!0,m(),r.then(function(){n._playLock=!1,g._unlocked=!0,t||(n._emit("play",a._id),n._loadQueue())}).catch(function(){n._playLock=!1,n._emit("playerror",a._id,"Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction."),a._ended=!0,a._paused=!0})):t||(n._playLock=!1,m(),n._emit("play",a._id),n._loadQueue()),g.playbackRate=a._rate,g.paused)return void n._emit("playerror",a._id,"Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction.");"__default"!==e||a._loop?n._endTimers[a._id]=setTimeout(n._ended.bind(n,a),c):(n._endTimers[a._id]=function(){n._ended(a),g.removeEventListener("ended",n._endTimers[a._id],!1)},g.addEventListener("ended",n._endTimers[a._id],!1))}catch(e){n._emit("playerror",a._id,e)}},_=window&&window.ejecta||!g.readyState&&i._navigator.isCocoonJS;if(g.readyState>=3||_)y();else{n._playLock=!0;var x=function(){y(),g.removeEventListener(i._canPlayEvent,x,!1)};g.addEventListener(i._canPlayEvent,x,!1),n._clearTimer(a._id)}}return a._id}n._ended(a)},pause:function(e){var t=this;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"pause",action:function(){t.pause(e)}}),t;for(var i=t._getSoundIds(e),n=0;n=0?t=parseInt(s[0],10):e=parseFloat(s[0]):s.length>=2&&(e=parseFloat(s[0]),t=parseInt(s[1],10)),!(void 0!==e&&e>=0&&e<=1))return(n=t?r._soundById(t):r._sounds[0])?n._volume:0;if("loaded"!==r._state||r._playLock)return r._queue.push({event:"volume",action:function(){r.volume.apply(r,s)}}),r;void 0===t&&(r._volume=e),t=r._getSoundIds(t);for(var o=0;o0?n/l:n),c=Date.now();e._fadeTo=i,e._interval=setInterval(function(){var r=(Date.now()-c)/n;c=Date.now(),a+=h*r,a=Math.max(0,a),a=Math.min(1,a),a=Math.round(100*a)/100,o._webAudio?e._volume=a:o.volume(a,e._id,!0),s&&(o._volume=a),(it&&a>=i)&&(clearInterval(e._interval),e._interval=null,e._fadeTo=null,o.volume(i,e._id),o._emit("fade",e._id))},u)},_stopFade:function(e){var t=this._soundById(e);return t&&t._interval&&(this._webAudio&&t._node.gain.cancelScheduledValues(i.ctx.currentTime),clearInterval(t._interval),t._interval=null,this.volume(t._fadeTo,e),t._fadeTo=null,this._emit("fade",e)),this},loop:function(){var e,t,i,n=arguments;if(0===n.length)return this._loop;if(1===n.length){if("boolean"!=typeof n[0])return!!(i=this._soundById(parseInt(n[0],10)))&&i._loop;e=n[0],this._loop=e}else 2===n.length&&(e=n[0],t=parseInt(n[1],10));for(var r=this._getSoundIds(t),s=0;s=0?t=parseInt(s[0],10):e=parseFloat(s[0])}else 2===s.length&&(e=parseFloat(s[0]),t=parseInt(s[1],10));if("number"!=typeof e)return(n=r._soundById(t))?n._rate:r._rate;if("loaded"!==r._state||r._playLock)return r._queue.push({event:"rate",action:function(){r.rate.apply(r,s)}}),r;void 0===t&&(r._rate=e),t=r._getSoundIds(t);for(var o=0;o=0?t=parseInt(r[0],10):n._sounds.length&&(t=n._sounds[0]._id,e=parseFloat(r[0]))}else 2===r.length&&(e=parseFloat(r[0]),t=parseInt(r[1],10));if(void 0===t)return n;if("loaded"!==n._state||n._playLock)return n._queue.push({event:"seek",action:function(){n.seek.apply(n,r)}}),n;var s=n._soundById(t);if(s){if(!("number"==typeof e&&e>=0)){if(n._webAudio){var o=n.playing(t)?i.ctx.currentTime-s._playStart:0,a=s._rateSeek?s._rateSeek-s._seek:0;return s._seek+(a+o*Math.abs(s._rate))}return s._node.currentTime}var h=n.playing(t);h&&n.pause(t,!0),s._seek=e,s._ended=!1,n._clearTimer(t),n._webAudio||!s._node||isNaN(s._node.duration)||(s._node.currentTime=e);var l=function(){n._emit("seek",t),h&&n.play(t,!0)};if(h&&!n._webAudio){var u=function(){n._playLock?setTimeout(u,0):l()};setTimeout(u,0)}else l()}return n},playing:function(e){if("number"==typeof e){var t=this._soundById(e);return!!t&&!t._paused}for(var i=0;i=0&&i._howls.splice(r,1);var o=!0;for(n=0;n=0){o=!1;break}return s&&o&&delete s[e._src],i.noAudio=!1,e._state="unloaded",e._sounds=[],e=null,null},on:function(e,t,i,n){var r=this["_on"+e];return"function"==typeof t&&r.push(n?{id:i,fn:t,once:n}:{id:i,fn:t}),this},off:function(e,t,i){var n=this["_on"+e],r=0;if("number"==typeof t&&(i=t,t=null),t||i)for(r=0;r=0;r--)n[r].id&&n[r].id!==t&&"load"!==e||(setTimeout(function(e){e.call(this,t,i)}.bind(this,n[r].fn),0),n[r].once&&this.off(e,n[r].fn,n[r].id));return this._loadQueue(e),this},_loadQueue:function(e){if(this._queue.length>0){var t=this._queue[0];t.event===e&&(this._queue.shift(),this._loadQueue()),e||t.action()}return this},_ended:function(e){var t=e._sprite;if(!this._webAudio&&e._node&&!e._node.paused&&!e._node.ended&&e._node.currentTime=0;i--){if(t<=e)return;this._sounds[i]._ended&&(this._webAudio&&this._sounds[i]._node&&this._sounds[i]._node.disconnect(0),this._sounds.splice(i,1),t--)}}},_getSoundIds:function(e){if(void 0===e){for(var t=[],i=0;i=0;if(i._scratchBuffer&&e.bufferSource&&(e.bufferSource.onended=null,e.bufferSource.disconnect(0),t))try{e.bufferSource.buffer=i._scratchBuffer}catch(e){}return e.bufferSource=null,this}};var r=function(e){this._parent=e,this.init()};r.prototype={init:function(){var e=this._parent;return this._muted=e._muted,this._loop=e._loop,this._volume=e._volume,this._rate=e._rate,this._seek=0,this._paused=!0,this._ended=!0,this._sprite="__default",this._id=++i._counter,e._sounds.push(this),this.create(),this},create:function(){var e=this._parent,t=i._muted||this._muted||this._parent._muted?0:this._volume;return e._webAudio?(this._node=void 0===i.ctx.createGain?i.ctx.createGainNode():i.ctx.createGain(),this._node.gain.setValueAtTime(t,i.ctx.currentTime),this._node.paused=!0,this._node.connect(i.masterGain)):(this._node=i._obtainHtml5Audio(),this._errorFn=this._errorListener.bind(this),this._node.addEventListener("error",this._errorFn,!1),this._loadFn=this._loadListener.bind(this),this._node.addEventListener(i._canPlayEvent,this._loadFn,!1),this._node.src=e._src,this._node.preload="auto",this._node.volume=t*i.volume(),this._node.load()),this},reset:function(){var e=this._parent;return this._muted=e._muted,this._loop=e._loop,this._volume=e._volume,this._rate=e._rate,this._seek=0,this._rateSeek=0,this._paused=!0,this._ended=!0,this._sprite="__default",this._id=++i._counter,this},_errorListener:function(){this._parent._emit("loaderror",this._id,this._node.error?this._node.error.code:0),this._node.removeEventListener("error",this._errorFn,!1)},_loadListener:function(){var e=this._parent;e._duration=Math.ceil(10*this._node.duration)/10,0===Object.keys(e._sprite).length&&(e._sprite={__default:[0,1e3*e._duration]}),"loaded"!==e._state&&(e._state="loaded",e._emit("load"),e._loadQueue()),this._node.removeEventListener(i._canPlayEvent,this._loadFn,!1)}};var s={},o=function(e){var t=e._src;if(s[t])return e._duration=s[t].duration,void l(e);if(/^data:[^;]+;base64,/.test(t)){for(var i=atob(t.split(",")[1]),n=new Uint8Array(i.length),r=0;r0?(s[t._src]=e,l(t,e)):n()};"undefined"!=typeof Promise&&1===i.ctx.decodeAudioData.length?i.ctx.decodeAudioData(e).then(r).catch(n):i.ctx.decodeAudioData(e,r,n)},l=function(e,t){t&&!e._duration&&(e._duration=t.duration),0===Object.keys(e._sprite).length&&(e._sprite={__default:[0,1e3*e._duration]}),"loaded"!==e._state&&(e._state="loaded",e._emit("load"),e._loadQueue())},u=function(){if(i.usingWebAudio){try{"undefined"!=typeof AudioContext?i.ctx=new AudioContext:"undefined"!=typeof webkitAudioContext?i.ctx=new webkitAudioContext:i.usingWebAudio=!1}catch(e){i.usingWebAudio=!1}i.ctx||(i.usingWebAudio=!1);var e=/iP(hone|od|ad)/.test(i._navigator&&i._navigator.platform),t=i._navigator&&i._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/),n=t?parseInt(t[1],10):null;if(e&&n&&n<9){var r=/safari/.test(i._navigator&&i._navigator.userAgent.toLowerCase());(i._navigator&&i._navigator.standalone&&!r||i._navigator&&!i._navigator.standalone&&!r)&&(i.usingWebAudio=!1)}i.usingWebAudio&&(i.masterGain=void 0===i.ctx.createGain?i.ctx.createGainNode():i.ctx.createGain(),i.masterGain.gain.setValueAtTime(i._muted?0:1,i.ctx.currentTime),i.masterGain.connect(i.ctx.destination)),i._setup()}};t.Howler=i,t.Howl=n,"undefined"!=typeof window?(window.HowlerGlobal=e,window.Howler=i,window.Howl=n,window.Sound=r):void 0!==commonjsGlobal&&(commonjsGlobal.HowlerGlobal=e,commonjsGlobal.Howler=i,commonjsGlobal.Howl=n,commonjsGlobal.Sound=r)}(), +/*! + * Spatial Plugin - Adds support for stereo and 3D audio where Web Audio is supported. + * + * howler.js v2.1.1 + * howlerjs.com + * + * (c) 2013-2018, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ +function(){var e;HowlerGlobal.prototype._pos=[0,0,0],HowlerGlobal.prototype._orientation=[0,0,-1,0,1,0],HowlerGlobal.prototype.stereo=function(e){if(!this.ctx||!this.ctx.listener)return this;for(var t=this._howls.length-1;t>=0;t--)this._howls[t].stereo(e);return this},HowlerGlobal.prototype.pos=function(e,t,i){return this.ctx&&this.ctx.listener?(t="number"!=typeof t?this._pos[1]:t,i="number"!=typeof i?this._pos[2]:i,"number"!=typeof e?this._pos:(this._pos=[e,t,i],void 0!==this.ctx.listener.positionX?(this.ctx.listener.positionX.setTargetAtTime(this._pos[0],Howler.ctx.currentTime,.1),this.ctx.listener.positionY.setTargetAtTime(this._pos[1],Howler.ctx.currentTime,.1),this.ctx.listener.positionZ.setTargetAtTime(this._pos[2],Howler.ctx.currentTime,.1)):this.ctx.listener.setPosition(this._pos[0],this._pos[1],this._pos[2]),this)):this},HowlerGlobal.prototype.orientation=function(e,t,i,n,r,s){if(!this.ctx||!this.ctx.listener)return this;var o=this._orientation;return t="number"!=typeof t?o[1]:t,i="number"!=typeof i?o[2]:i,n="number"!=typeof n?o[3]:n,r="number"!=typeof r?o[4]:r,s="number"!=typeof s?o[5]:s,"number"!=typeof e?o:(this._orientation=[e,t,i,n,r,s],void 0!==this.ctx.listener.forwardX?(this.ctx.listener.forwardX.setTargetAtTime(e,Howler.ctx.currentTime,.1),this.ctx.listener.forwardY.setTargetAtTime(t,Howler.ctx.currentTime,.1),this.ctx.listener.forwardZ.setTargetAtTime(i,Howler.ctx.currentTime,.1),this.ctx.listener.upX.setTargetAtTime(e,Howler.ctx.currentTime,.1),this.ctx.listener.upY.setTargetAtTime(t,Howler.ctx.currentTime,.1),this.ctx.listener.upZ.setTargetAtTime(i,Howler.ctx.currentTime,.1)):this.ctx.listener.setOrientation(e,t,i,n,r,s),this)},Howl.prototype.init=(e=Howl.prototype.init,function(t){return this._orientation=t.orientation||[1,0,0],this._stereo=t.stereo||null,this._pos=t.pos||null,this._pannerAttr={coneInnerAngle:void 0!==t.coneInnerAngle?t.coneInnerAngle:360,coneOuterAngle:void 0!==t.coneOuterAngle?t.coneOuterAngle:360,coneOuterGain:void 0!==t.coneOuterGain?t.coneOuterGain:0,distanceModel:void 0!==t.distanceModel?t.distanceModel:"inverse",maxDistance:void 0!==t.maxDistance?t.maxDistance:1e4,panningModel:void 0!==t.panningModel?t.panningModel:"HRTF",refDistance:void 0!==t.refDistance?t.refDistance:1,rolloffFactor:void 0!==t.rolloffFactor?t.rolloffFactor:1},this._onstereo=t.onstereo?[{fn:t.onstereo}]:[],this._onpos=t.onpos?[{fn:t.onpos}]:[],this._onorientation=t.onorientation?[{fn:t.onorientation}]:[],e.call(this,t)}),Howl.prototype.stereo=function(e,i){var n=this;if(!n._webAudio)return n;if("loaded"!==n._state)return n._queue.push({event:"stereo",action:function(){n.stereo(e,i)}}),n;var r=void 0===Howler.ctx.createStereoPanner?"spatial":"stereo";if(void 0===i){if("number"!=typeof e)return n._stereo;n._stereo=e,n._pos=[e,0,0]}for(var s=n._getSoundIds(i),o=0;o3){var r="melonJS: failed loading "+e;if(!1!==me.sys.stopOnAudioError)throw new Error(r);me.audio.disable(),i&&i(),console.log(r+", disabling audio")}else t[e].load()}).call(me.audio,e.name,s)},onload:function(){n=0,r&&r()}}),1},e.play=function(e,i,n,r){var s=t[e];if(s&&void 0!==s){var o=s.play();return"boolean"==typeof i&&s.loop(i,o),s.volume("number"==typeof r?me.Math.clamp(r,0,1):howler_1.volume(),o),"function"==typeof n&&(!0===i?s.on("end",n,o):s.once("end",n,o)),o}throw new Error("audio clip "+e+" does not exist")},e.fade=function(e,i,n,r,s){var o=t[e];if(!o||void 0===o)throw new Error("audio clip "+e+" does not exist");o.fade(i,n,r,s)},e.seek=function(e,i,n){var r=t[e];if(r&&void 0!==r)return r.seek.apply(r,Array.prototype.slice.call(arguments,1));throw new Error("audio clip "+e+" does not exist")},e.rate=function(e,i,n){var r=t[e];if(r&&void 0!==r)return r.rate.apply(r,Array.prototype.slice.call(arguments,1));throw new Error("audio clip "+e+" does not exist")},e.stop=function(e,i){var n=t[e];if(!n||void 0===n)throw new Error("audio clip "+e+" does not exist");n.stop(i),n.off("end",void 0,i)},e.pause=function(e,i){var n=t[e];if(!n||void 0===n)throw new Error("audio clip "+e+" does not exist");n.pause(i)},e.resume=function(e,i){var n=t[e];if(!n||void 0===n)throw new Error("audio clip "+e+" does not exist");n.play(i)},e.playTrack=function(e,t){return i=e,me.audio.play(i,!0,null,t)},e.stopTrack=function(){null!==i&&(t[i].stop(),i=null)},e.pauseTrack=function(){null!==i&&t[i].pause()},e.resumeTrack=function(){null!==i&&t[i].play()},e.getCurrentTrack=function(){return i},e.setVolume=function(e){howler_1.volume(e)},e.getVolume=function(){return howler_1.volume()},e.mute=function(e,i,n){n=void 0===n||!!n;var r=t[e];if(!r||void 0===r)throw new Error("audio clip "+e+" does not exist");r.mute(n,i)},e.unmute=function(t,i){e.mute(t,i,!1)},e.muteAll=function(){howler_1.mute(!0)},e.unmuteAll=function(){howler_1.mute(!1)},e.muted=function(){return howler_1._muted},e.unload=function(e){return e in t&&(t[e].unload(),delete t[e],!0)},e.unloadAll=function(){for(var i in t)t.hasOwnProperty(i)&&e.unload(i)},e}(),me.video=function(){var e={},t=0,i=1,n=0,r=0,s=1/0,o=1/0,a={wrapper:void 0,renderer:2,doubleBuffering:!1,autoScale:!1,scale:1,scaleMethod:"fit",transparent:!1,blendMode:"normal",antiAlias:!1,failIfMajorPerformanceCaveat:!0,subPixel:!1,verbose:!1,consoleHeader:!0};return e.CANVAS=0,e.WEBGL=1,e.AUTO=2,e.init=function(t,s,o){if(!me.initialized)throw new Error("me.video.init() called before engine initialization.");(a=Object.assign(a,o||{})).doubleBuffering=!!a.doubleBuffering,a.useParentDOMSize=!!a.useParentDOMSize,a.autoScale="auto"===a.scale||!1,a.transparent=!!a.transparent,a.antiAlias=!!a.antiAlias,a.failIfMajorPerformanceCaveat=!!a.failIfMajorPerformanceCaveat,a.subPixel=!!a.subPixel,a.verbose=!!a.verbose,0!==a.scaleMethod.search(/^(fill-(min|max)|fit|flex(-(width|height))?|stretch)$/)&&(a.scaleMethod="fit"),!0===me.game.HASH.webgl&&(a.renderer=e.WEBGL),a.scale=a.autoScale?1:+a.scale||1,me.sys.scale=new me.Vector2d(a.scale,a.scale),(a.autoScale||1!==a.scale)&&(a.doubleBuffering=!0),i=t/s,n=t,r=s;var h,l=t*me.sys.scale.x,u=s*me.sys.scale.y;if(a.zoomX=l,a.zoomY=u,window.addEventListener("resize",me.utils.function.throttle(function(e){me.event.publish(me.event.WINDOW_ONRESIZE,[e])},100),!1),window.addEventListener("orientationchange",function(e){me.event.publish(me.event.WINDOW_ONORIENTATION_CHANGE,[e])},!1),window.addEventListener("onmozorientationchange",function(e){me.event.publish(me.event.WINDOW_ONORIENTATION_CHANGE,[e])},!1),void 0!==window.screen&&(window.screen.onorientationchange=function(e){me.event.publish(me.event.WINDOW_ONORIENTATION_CHANGE,[e])}),window.addEventListener("scroll",me.utils.function.throttle(function(e){me.video.renderer.updateBounds(),me.event.publish(me.event.WINDOW_ONSCROLL,[e])},100),!1),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)),h=!0===me.device.ejecta?document.getElementById("canvas"):void 0!==window.canvas?window.canvas:e.createCanvas(l,u),o.wrapper&&(a.wrapper=document.getElementById(o.wrapper)),a.wrapper||(a.wrapper=document.body),a.wrapper.appendChild(h),void 0===h.getContext)return!1;switch(a.renderer){case e.AUTO:case e.WEBGL:this.renderer=function(e,t,i,n){try{return new me.WebGLRenderer(e,t,i,n)}catch(r){return new me.CanvasRenderer(e,t,i,n)}}(h,t,s,a);break;default:this.renderer=new me.CanvasRenderer(h,t,s,a)}var c=me.device.devicePixelRatio;if(c>1&&(h.style.width=h.width/c+"px",h.style.height=h.height/c+"px"),window.getComputedStyle){var d=window.getComputedStyle(h,null);me.video.setMaxSize(parseInt(d.maxWidth,10),parseInt(d.maxHeight,10))}if(me.game.init(),me.video.onresize(),"MutationObserver"in window&&new MutationObserver(me.video.onresize.bind(me.video)).observe(a.wrapper,{attributes:!1,childList:!0,subtree:!0}),!1!==o.consoleHeader){var f=me.video.renderer instanceof me.CanvasRenderer?"CANVAS":"WebGL",p=me.device.hasWebAudio?"Web Audio":"HTML5 Audio";console.log(me.mod+" "+me.version+" | http://melonjs.org"),console.log(f+" | "+p+" | pixel ratio "+me.device.devicePixelRatio+" | "+(me.device.isMobile?"mobile":"desktop")+" | "+me.device.getScreenOrientation()+" | "+me.device.language),console.log("resolution: requested "+t+"x"+s+", got "+me.video.renderer.getWidth()+"x"+me.video.renderer.getHeight())}return!0},e.setMaxSize=function(e,t){s=e||1/0,o=t||1/0,me.utils.function.defer(me.video.onresize,me.video)},e.createCanvas=function(e,t){if(0===e||0===t)throw new Error("width or height was zero, Canvas could not be initialized !");var i=document.createElement("canvas");return i.width=e,i.height=t,i},e.getWrapper=function(){return a.wrapper},e.onresize=function(){var e=1,h=1;if(a.autoScale){var l,u,c=me.video.renderer.getScreenCanvas().parentNode;if(void 0!==c)if(a.useParentDOMSize&&"function"==typeof c.getBoundingClientRect){var d=c.getBoundingClientRect();l=d.width||d.right-d.left,u=d.height||d.bottom-d.top}else l=c.width,u=c.height;var f=Math.min(s,l||window.innerWidth),p=Math.min(o,u||window.innerHeight),m=f/p,g=1/0,v=1/0;"fill-min"===a.scaleMethod&&m>i||"fill-max"===a.scaleMethod&&mi||"flex-height"===a.scaleMethod?(e=h=p/(v=Math.min(o,n*(p/f))),v=~~(v+.5),this.renderer.resize(n,v)):"flex"===a.scaleMethod?this.renderer.resize(f,p):"stretch"===a.scaleMethod?(e=f/n,h=p/r):e=h=m0&&e.top0},resize:function(e,t){e===this.backBufferCanvas.width&&t===this.backBufferCanvas.height||(this.backBufferCanvas.width=e,this.backBufferCanvas.height=t,this.currentScissor[0]=0,this.currentScissor[1]=0,this.currentScissor[2]=e,this.currentScissor[3]=t,me.event.publish(me.event.CANVAS_ONRESIZE,[e,t])),this.updateBounds()},setAntiAlias:function(e,t){var i=e.canvas;me.agent.setPrefixed("imageSmoothingEnabled",!0===t,e),!0!==t?(i.style["image-rendering"]="pixelated",i.style["image-rendering"]="crisp-edges",i.style["image-rendering"]="-moz-crisp-edges",i.style["image-rendering"]="-o-crisp-edges",i.style["image-rendering"]="-webkit-optimize-contrast",i.style.msInterpolationMode="nearest-neighbor"):i.style["image-rendering"]="auto"},stroke:function(e,t){"Rectangle"===e.shapeType?this.strokeRect(e.left,e.top,e.width,e.height,t):e instanceof me.Line||e instanceof me.Polygon?this.strokePolygon(e,t):e instanceof me.Ellipse&&this.strokeEllipse(e.pos.x,e.pos.y,e.radiusV.x,e.radiusV.y,t)},fill:function(e){this.stroke(e,!0)},setMask:function(e){},clearMask:function(){},setTint:function(e){this.currentTint.copy(e)},clearTint:function(){this.currentTint.setColor(255,255,255,1)},drawFont:function(){}}),me.Renderer.prototype.Texture=me.Object.extend({init:function(e,t,i){if(this.format=null,this.sources=new Map,this.atlases=new Map,void 0!==e)for(var n in e=Array.isArray(e)?e:[e]){var r=e[n];if(void 0!==r.meta){if(r.meta.app.includes("texturepacker")){if(this.format="texturepacker",void 0===t){var s=me.loader.getImage(r.meta.image);if(!s)throw new Error("Atlas texture '"+s+"' not found");this.sources.set(r.meta.image,s)}else this.sources.set(r.meta.image||"default","string"==typeof t?me.loader.getImage(t):t);this.repeat="no-repeat"}else if(r.meta.app.includes("ShoeBox")){if(!r.meta.exporter||!r.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","string"==typeof t?me.loader.getImage(t):t)}else r.meta.app.includes("melonJS")&&(this.format="melonJS",this.repeat=r.meta.repeat||"no-repeat",this.sources.set("default","string"==typeof t?me.loader.getImage(t):t));this.atlases.set(r.meta.image||"default",this.parse(r))}else void 0!==r.framewidth&&void 0!==r.frameheight&&(this.format="Spritesheet (fixed cell size)",this.repeat="no-repeat",void 0!==t&&(r.image="string"==typeof t?me.loader.getImage(t):t),this.atlases.set("default",this.parseFromSpriteSheet(r)),this.sources.set("default",r.image))}if(0===this.atlases.length)throw new Error("texture atlas format not supported");if(!1!==i){t=Array.isArray(t)?t:[t];var o=!0,a=!1,h=void 0;try{for(var l,u=this.sources[Symbol.iterator]();!(o=(l=u.next()).done);o=!0){var c=l.value;i instanceof me.Renderer.TextureCache?i.set(c,this):me.video.renderer.cache.set(c,this)}}catch(e){a=!0,h=e}finally{try{o||null==u.return||u.return()}finally{if(a)throw h}}}},createAtlas:function(e,t,i,n){return{meta:{app:"melonJS",size:{w:e,h:t},repeat:n||"no-repeat",image:"default"},frames:[{filename:i||"default",frame:{x:0,y:0,w:e,h:t}}]}},parse:function(e){var t={},i=this;return e.frames.forEach(function(n){if(n.hasOwnProperty("filename")){var r,s,o=n.frame,a=n.spriteSourceSize&&n.sourceSize&&n.pivot;a&&(r=n.sourceSize.w*n.pivot.x-(n.trimmed?n.spriteSourceSize.x:0),s=n.sourceSize.h*n.pivot.y-(n.trimmed?n.spriteSourceSize.y:0)),t[n.filename]={name:n.filename,texture:e.meta.image||"default",offset:new me.Vector2d(o.x,o.y),anchorPoint:a?new me.Vector2d(r/o.w,s/o.h):null,trimmed:!!n.trimmed,width:o.w,height:o.h,angle:!0===n.rotated?-me.Math.ETA:0},i.addUvsMap(t,n.filename,e.meta.size.w,e.meta.size.h)}}),t},parseFromSpriteSheet:function(e){var t={},i=e.image,n=e.spacing||0,r=e.margin||0,s=i.width,o=i.height,a=me.pool.pull("me.Vector2d",~~((s-r+n)/(e.framewidth+n)),~~((o-r+n)/(e.frameheight+n)));s%(e.framewidth+n)==0&&o%(e.frameheight+n)==0||(s=a.x*(e.framewidth+n),o=a.y*(e.frameheight+n),console.warn("Spritesheet Texture for image: "+i.src+" is not divisible by "+(e.framewidth+n)+"x"+(e.frameheight+n)+", truncating effective size to "+s+"x"+o));for(var h=0,l=a.x*a.y;h=this.max_size)throw new Error("Texture cache overflow: "+this.max_size+" texture units available.")},get:function(e,t){return this.cache.has(e)||(t||(t=me.video.renderer.Texture.prototype.createAtlas.apply(me.video.renderer.Texture.prototype,[e.width,e.height,e.src?me.utils.file.getBasename(e.src):void 0])),this.set(e,new me.video.renderer.Texture(t,e,!1))),this.cache.get(e)},set:function(e,t){var i=e.width,n=e.height;if(!me.Math.isPowerOfTwo(i)||!me.Math.isPowerOfTwo(n)){var r=void 0!==e.src?e.src:e;console.warn("[Texture] "+r+" is not a POT texture ("+i+"x"+n+")")}this.validate(),this.cache.set(e,t),this.units.set(t,this.length++)},getUnit:function(e){return this.units.get(e)}}),me.CanvasRenderer=me.Renderer.extend({init:function(e,t,i,n){return this._super(me.Renderer,"init",[e,t,i,n]),this.context=this.getContext2d(this.canvas,this.settings.transparent),this.settings.doubleBuffering?(this.backBufferCanvas=me.video.createCanvas(t,i),this.backBufferContext2D=this.getContext2d(this.backBufferCanvas),this.settings.transparent&&(this.context.globalCompositeOperation="copy")):(this.backBufferCanvas=this.canvas,this.backBufferContext2D=this.context),this.setBlendMode(this.settings.blendMode),this.setColor(this.currentColor),this.cache=new me.Renderer.TextureCache,!1===this.settings.textureSeamFix||this.settings.antiAlias||(this.uvOffset=1),this},reset:function(){this._super(me.Renderer,"reset")},resetTransform:function(){this.backBufferContext2D.setTransform(1,0,0,1,0,0)},setBlendMode:function(e,t){switch(t=t||this.getContext(),this.currentBlendMode=e,e){case"multiply":t.globalCompositeOperation="multiply";break;default:t.globalCompositeOperation="source-over",this.currentBlendMode="normal"}},clear:function(){this.settings.transparent&&this.clearColor("rgba(0,0,0,0)",!0)},flush:function(){this.settings.doubleBuffering&&this.context.drawImage(this.backBufferCanvas,0,0,this.backBufferCanvas.width,this.backBufferCanvas.height,0,0,this.gameWidthZoom,this.gameHeightZoom)},clearColor:function(e,t){this.save(),this.resetTransform(),this.backBufferContext2D.globalCompositeOperation=t?"copy":"source-over",this.backBufferContext2D.fillStyle=e instanceof me.Color?e.toRGBA():e,this.fillRect(0,0,this.backBufferCanvas.width,this.backBufferCanvas.height),this.restore()},clearRect:function(e,t,i,n){this.backBufferContext2D.clearRect(e,t,i,n)},createPattern:function(e,t){return this.backBufferContext2D.createPattern(e,t)},drawImage:function(e,t,i,n,r,s,o,a,h){this.backBufferContext2D.globalAlpha<1/255||(void 0===n?(n=a=e.width,r=h=e.height,s=t,o=i,t=0,i=0):void 0===s&&(s=t,o=i,a=n,h=r,n=e.width,r=e.height,t=0,i=0),!1===this.settings.subPixel&&(s=~~s,o=~~o),this.backBufferContext2D.drawImage(e,t,i,n,r,s,o,a,h))},drawPattern:function(e,t,i,n,r){if(!(this.backBufferContext2D.globalAlpha<1/255)){var s=this.backBufferContext2D.fillStyle;this.backBufferContext2D.fillStyle=e,this.backBufferContext2D.fillRect(t,i,n,r),this.backBufferContext2D.fillStyle=s}},strokeArc:function(e,t,i,n,r,s,o){var a=this.backBufferContext2D;a.globalAlpha<1/255||(this.translate(e+i,t+i),a.beginPath(),a.arc(0,0,i,n,r,s||!1),a[!0===o?"fill":"stroke"](),a.closePath(),this.translate(-(e+i),-(t+i)))},fillArc:function(e,t,i,n,r,s){this.strokeArc(e,t,i,n,r,s||!1,!0)},strokeEllipse:function(e,t,i,n,r){var s=this.backBufferContext2D;if(!(s.globalAlpha<1/255)){var o=e-i,a=e+i,h=t-n,l=t+n,u=.551784*i,c=.551784*n,d=e-u,f=e+u,p=t-c,m=t+c;s.beginPath(),s.moveTo(e,h),s.bezierCurveTo(f,h,a,p,a,t),s.bezierCurveTo(a,m,f,l,e,l),s.bezierCurveTo(d,l,o,m,o,t),s.bezierCurveTo(o,p,d,h,e,h),s[!0===r?"fill":"stroke"]()}},fillEllipse:function(e,t,i,n){this.strokeEllipse(e,t,i,n,!0)},strokeLine:function(e,t,i,n){var r=this.backBufferContext2D;r<1/255||(r.beginPath(),r.moveTo(e,t),r.lineTo(i,n),r.stroke())},fillLine:function(e,t,i,n){this.strokeLine(e,t,i,n)},strokePolygon:function(e,t){var i=this.backBufferContext2D;if(!(i.globalAlpha<1/255)){var n;this.translate(e.pos.x,e.pos.y),i.beginPath(),i.moveTo(e.points[0].x,e.points[0].y);for(var r=1;r1&&(this.canvas.style.width=this.canvas.width/me.device.devicePixelRatio+"px",this.canvas.style.height=this.canvas.height/me.device.devicePixelRatio+"px"),this.settings.doubleBuffering&&this.settings.transparent?this.context.globalCompositeOperation="copy":this.setBlendMode(this.settings.blendMode,this.context),this.setAntiAlias(this.context,this.settings.antiAlias),this.flush()},save:function(){this.backBufferContext2D.save()},restore:function(){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},rotate:function(e){this.backBufferContext2D.rotate(e)},scale:function(e,t){this.backBufferContext2D.scale(e,t)},setColor:function(e){this.backBufferContext2D.strokeStyle=this.backBufferContext2D.fillStyle=e instanceof me.Color?e.toRGBA():e},setGlobalAlpha:function(e){this.backBufferContext2D.globalAlpha=this.currentColor.glArray[3]=e},setLineWidth:function(e){this.backBufferContext2D.lineWidth=e},setTransform:function(e){this.resetTransform(),this.transform(e)},transform:function(e){var t=e.val,i=t[6],n=t[7];!1===this.settings.subPixel&&(i=~~i,n=~~n),this.backBufferContext2D.transform(t[0],t[1],t[3],t[4],i,n)},translate:function(e,t){!1===this.settings.subPixel?this.backBufferContext2D.translate(~~e,~~t):this.backBufferContext2D.translate(e,t)},clipRect:function(e,t,i,n){var r=this.backBufferCanvas;if(0!==e||0!==t||i!==r.width||n!==r.height){var s=this.currentScissor;if(s[0]!==e||s[1]!==t||s[2]!==i||s[3]!==n){var o=this.backBufferContext2D;o.beginPath(),o.rect(e,t,i,n),o.clip(),s[0]=e,s[1]=t,s[2]=i,s[3]=n}}},setMask:function(e){var t=this.backBufferContext2D,i=e.pos.x,n=e.pos.y;if(e instanceof me.Ellipse){var r=e.radiusV.x,s=e.radiusV.y,o=i-r,a=i+r,h=n-s,l=n+s,u=.551784*r,c=.551784*s,d=i-u,f=i+u,p=n-c,m=n+c;t.beginPath(),t.moveTo(i,h),t.bezierCurveTo(f,h,a,p,a,n),t.bezierCurveTo(a,m,f,l,i,l),t.bezierCurveTo(d,l,o,m,o,n),t.bezierCurveTo(o,p,d,h,i,h)}else{var g;t.save(),t.beginPath(),t.moveTo(i+e.points[0].x,n+e.points[0].y);for(var v=1;v1?(this.canvas.style.width=i/me.device.devicePixelRatio+"px",this.canvas.style.height=n/me.device.devicePixelRatio+"px"):(this.canvas.style.width=i+"px",this.canvas.style.height=n+"px"),this.compositor.setProjection(this.canvas.width,this.canvas.height)},restore:function(){if(0!==this._matrixStack.length){var e=this._colorStack.pop(),t=this._matrixStack.pop();this.currentColor.copy(e),this.currentTransform.copy(t),me.pool.push(e),me.pool.push(t)}0!==this._scissorStack.length?this.currentScissor.set(this._scissorStack.pop()):(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)},save:function(){this._colorStack.push(this.currentColor.clone()),this._matrixStack.push(this.currentTransform.clone()),this.gl.isEnabled(this.gl.SCISSOR_TEST)&&this._scissorStack.push(this.currentScissor.slice())},rotate:function(e){this.currentTransform.rotate(e)},scale:function(e,t){this.currentTransform.scale(e,t)},setAntiAlias:function(e,t){this._super(me.Renderer,"setAntiAlias",[e,t])},setGlobalAlpha:function(e){this.currentColor.glArray[3]=e},setColor:function(e){var t=this.currentColor.glArray[3];this.currentColor.copy(e),this.currentColor.glArray[3]*=t},setLineWidth:function(e){this.getScreenContext().lineWidth(e)},strokeArc:function(e,t,i,n,r,s,o){!0===o?this.fillArc(e,t,i,n,r,s):console.warn("strokeArc() is not implemented")},fillArc:function(e,t,i,n,r,s){console.warn("fillArc() is not implemented")},strokeEllipse:function(e,t,i,n,r){if(!0===r)this.fillEllipse(e,t,i,n);else{var s,o=Math.floor(24*Math.sqrt(i))||Math.floor(12*Math.sqrt(i+n)),a=me.Math.TAU/o,h=this._glPoints;for(s=h.length;s=16e3&&this.flush(),this.length>=this.sbSize&&this.resizeSB();var h=this.matrix,l=this.v[0].set(i,n),u=this.v[1].set(i+r,n),c=this.v[2].set(i,n+s),d=this.v[3].set(i+r,n+s);h.isIdentity()||(h.multiplyVector(l),h.multiplyVector(u),h.multiplyVector(c),h.multiplyVector(d));var f=this.sbIndex,p=f+9,m=p+9,g=m+9;this.stream[f+0+0]=l.x,this.stream[f+0+1]=l.y,this.stream[p+0+0]=u.x,this.stream[p+0+1]=u.y,this.stream[m+0+0]=c.x,this.stream[m+0+1]=c.y,this.stream[g+0+0]=d.x,this.stream[g+0+1]=d.y,this.stream.set(a,f+2),this.stream.set(a,p+2),this.stream.set(a,m+2),this.stream.set(a,g+2);var v=this.uploadTexture(e);this.stream[f+6]=this.stream[p+6]=this.stream[m+6]=this.stream[g+6]=v;var y=e.getUVs(t);this.stream[f+7+0]=y[0],this.stream[f+7+1]=y[1],this.stream[p+7+0]=y[2],this.stream[p+7+1]=y[1],this.stream[m+7+0]=y[0],this.stream[m+7+1]=y[3],this.stream[g+7+0]=y[2],this.stream[g+7+1]=y[3],this.sbIndex+=36,this.length++}},flush:function(){if(this.length){var e=this.gl,t=9*this.length*4;e.bufferData(e.ARRAY_BUFFER,this.stream.subarray(0,t),e.STREAM_DRAW),e.drawElements(e.TRIANGLES,6*this.length,e.UNSIGNED_SHORT,0),this.sbIndex=0,this.length=0}},drawTriangle:function(e,t,i){var n=this.gl;t=t||e.length,this.useShader(this.primitiveShader);for(var r=0,s=this.matrix,o=s.isIdentity(),a=0;a?~%])\s*/g,"$1")}me.GLShader=me.Object.extend({init:function(r,s,o,a){return this.gl=r,this.vertex=i(n(s),a||me.device.getMaxShaderPrecision(this.gl)),this.fragment=i(n(o),a||me.device.getMaxShaderPrecision(this.gl)),this.program=function(t,i,n){var r=e(t,t.VERTEX_SHADER,i),s=e(t,t.FRAGMENT_SHADER,n),o=t.createProgram();if(t.attachShader(o,r),t.attachShader(o,s),t.linkProgram(o),!t.getProgramParameter(o,t.LINK_STATUS))throw new Error("Error initializing Shader "+this+"\ngl.VALIDATE_STATUS: "+t.getProgramParameter(o,t.VALIDATE_STATUS)+"\ngl.getError()"+t.getError()+"\ngl.getProgramInfoLog()"+t.getProgramInfoLog(o));return t.useProgram(o),t.deleteShader(r),t.deleteShader(s),o}(this.gl,this.vertex,this.fragment),this.attributes=function(e,t){for(var i,n={},r=/attribute\s+\w+\s+(\w+)/g,s=[];i=r.exec(t.vertex);)s.push(i[1]);return s.forEach(function(i){n[i]=e.getAttribLocation(t.program,i),e.enableVertexAttribArray(n[i])}),n}(this.gl,this),this.uniforms=function(e,i){var n,r={},s=/uniform\s+(\w+)\s+(\w+)/g,o={},a={},h={};return[i.vertex,i.fragment].forEach(function(e){for(;n=s.exec(e);)o[n[2]]=n[1]}),Object.keys(o).forEach(function(n){var r=o[n];h[n]=e.getUniformLocation(i.program,n),a[n]={get:function(e){return function(){return h[e]}}(n),set:function(t,i,n){return 0===i.indexOf("mat")?function(i){e[n](h[t],!1,i)}:function(i){var r=n;i.length&&"v"!==n.substr(-1)&&(r+="v"),e[r](h[t],i)}}(n,r,"uniform"+t[r])}}),Object.defineProperties(r,a),r}(this.gl,this),me.event.subscribe(me.event.WEBGL_ONCONTEXT_LOST,this.destroy.bind(this)),this},bind:function(){this.gl.useProgram(this.program)},destroy:function(){this.uniforms=null,this.attributes=null,this.gl.deleteProgram(this.program),this.vertex=null,this.fragment=null}})}(),me.PrimitiveGLShader=me.GLShader.extend({init:function(e){return this._super(me.GLShader,"init",[e,["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 color","varying vec4 vColor;","void main(void) {"," gl_FragColor = vColor;","}"].join("\n")]),this}}),me.QuadGLShader=me.GLShader.extend({init:function(e,t){return this._super(me.GLShader,"init",[e,["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"),["uniform sampler2D uSampler["+t+"];","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 < "+(t-1)+"; i++) {"," if (texture == i) {"," gl_FragColor = texture2D(uSampler[i], vRegion) * vColor;"," return;"," }"," gl_FragColor = texture2D(uSampler["+(t-1)+"], vRegion) * vColor;"," };"," }","}"].join("\n")]),this}}),me.input={_preventDefaultFn:function(e){return e.stopPropagation?e.stopPropagation():e.cancelBubble=!0,e.preventDefault?e.preventDefault():e.returnValue=!1,!1},preventDefault:!0},function(e){e._KeyBinding={};var t={},i={},n={},r={},s={},o=!1;e._enableKeyboardEvent=function(){o||(window.addEventListener("keydown",e._keydown,!1),window.addEventListener("keyup",e._keyup,!1),o=!0)},e._keydown=function(i,o,a){o=o||i.keyCode||i.button;var h=e._KeyBinding[o];if(me.event.publish(me.event.KEYDOWN,[h,o,!h||!n[h]]),h){if(!n[h]){var l=void 0!==a?a:o;r[h][l]||(t[h]++,r[h][l]=!0)}return!s[o]||e._preventDefaultFn(i)}return!0},e._keyup=function(i,o,a){o=o||i.keyCode||i.button;var h=e._KeyBinding[o];if(me.event.publish(me.event.KEYUP,[h,o]),h){var l=void 0!==a?a:o;return r[h][l]=void 0,t[h]>0&&t[h]--,n[h]=!1,!s[o]||e._preventDefaultFn(i)}return!0},e.KEY={BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,PAUSE:19,CAPS_LOCK:20,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,PRINT_SCREEN:42,INSERT:45,DELETE:46,NUM0:48,NUM1:49,NUM2:50,NUM3:51,NUM4:52,NUM5:53,NUM6:54,NUM7:55,NUM8:56,NUM9:57,A:65,B:66,C:67,D:68,E:69,F:70,G:71,H:72,I:73,J:74,K:75,L:76,M:77,N:78,O:79,P:80,Q:81,R:82,S:83,T:84,U:85,V:86,W:87,X:88,Y:89,Z:90,WINDOW_KEY:91,NUMPAD0:96,NUMPAD1:97,NUMPAD2:98,NUMPAD3:99,NUMPAD4:100,NUMPAD5:101,NUMPAD6:102,NUMPAD7:103,NUMPAD8:104,NUMPAD9:105,MULTIPLY:106,ADD:107,SUBSTRACT:109,DECIMAL:110,DIVIDE:111,F1:112,F2:113,F3:114,F4:115,F5:116,F6:117,F7:118,F8:119,F9:120,F10:121,F11:122,F12:123,TILDE:126,NUM_LOCK:144,SCROLL_LOCK:145,SEMICOLON:186,PLUS:187,COMMA:188,MINUS:189,PERIOD:190,FORWAND_SLASH:191,GRAVE_ACCENT:192,OPEN_BRACKET:219,BACK_SLASH:220,CLOSE_BRACKET:221,SINGLE_QUOTE:222},e.isKeyPressed=function(e){return!(!t[e]||n[e])&&(i[e]&&(n[e]=!0),!0)},e.keyStatus=function(e){return t[e]>0},e.triggerKeyEvent=function(t,i){i?e._keydown({},t):e._keyup({},t)},e.bindKey=function(o,a,h,l){e._enableKeyboardEvent(),"boolean"!=typeof l&&(l=e.preventDefault),e._KeyBinding[o]=a,s[o]=l,t[a]=0,i[a]=h||!1,n[a]=!1,r[a]={}},e.unlockKey=function(e){n[e]=!1},e.unbindKey=function(n){var o=e._KeyBinding[n];t[o]=0,i[o]=!1,r[o]={},e._KeyBinding[n]=null,s[n]=null}}(me.input),viewportOffset=new me.Vector2d,me.Pointer=me.Rect.extend({init:function(e,t,i,n){this.event=void 0,this.type=void 0,this.button=0,this.isPrimary=!1,this.pageX=0,this.pageY=0,this.clientX=0,this.clientY=0,this.gameX=0,this.gameY=0,this.gameScreenX=0,this.gameScreenY=0,this.gameWorldX=0,this.gameWorldY=0,this.gameLocalX=0,this.gameLocalY=0,this.pointerId=void 0,this._super(me.Rect,"init",[e||0,t||0,i||1,n||1])},setEvent:function(e,t,i,n,r,s){var o=1,a=1;this.event=e,this.pageX=t||0,this.pageY=i||0,this.clientX=n||0,this.clientY=r||0,me.input.globalToLocal(this.pageX,this.pageY,this.pos),this.isNormalized=!me.device.PointerEvent||me.device.PointerEvent&&!(e instanceof window.PointerEvent),"wheel"===e.type&&(this.deltaMode=1,this.deltaX=e.deltaX,this.deltaY=-.025*e.wheelDelta,e.wheelDeltaX&&(this.deltaX=-.025*e.wheelDeltaX)),this.pointerId=void 0!==s?s:1,this.isPrimary=void 0===e.isPrimary||e.isPrimary,this.button=e.button||0,this.type=e.type,this.gameScreenX=this.pos.x,this.gameScreenY=this.pos.y,void 0!==me.game.viewport&&me.game.viewport.localToWorld(this.gameScreenX,this.gameScreenY,viewportOffset),this.gameWorldX=viewportOffset.x,this.gameWorldY=viewportOffset.y,!1===this.isNormalized?(o=e.width||1,a=e.height||1):"number"==typeof e.radiusX&&(o=2*e.radiusX||1,a=2*e.radiusY||1),this.resize(o,a)}}),function(e){var t=[],i=new Map,n=new me.Rect(0,0,1,1),r=!1,s=0,o=[],a=["wheel"],h=["pointermove","mousemove","touchmove"],l=["pointerdown","mousedown","touchstart"],u=["pointerup","mouseup","touchend"],c=["pointercancel","mousecancel","touchcancel"],d=["pointerenter","mouseenter","touchenter"],f=["pointerover","mouseover","touchover"],p=["pointerleave","mouseleave","touchleave"],m=[a[0],h[0],l[0],u[0],c[0],d[0],f[0],p[0]],g=[a[0],h[1],l[1],u[1],c[1],d[1],f[1],p[1]],v=[h[2],l[2],u[2],c[2],d[2],f[2],p[2]],y={wheel:a,pointermove:h,pointerdown:l,pointerup:u,pointercancel:c,pointerenter:d,pointerover:f,pointerleave:p},_=[];function x(){if(!r){for(var i=0;i=0&&(r=e.callbacks[t][s]);s--)if(!1===r(i))return!0}return!1}function E(r){for(var a=!1;r.length>0;){var l=r.pop();if(t.push(l),void 0!==l.event.timeStamp){if(l.event.timeStamp0;)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(!(o>4)+"0123456789ABCDEF".charAt(e%16)},rgbaRx=/^rgba?\((\d+), ?(\d+), ?(\d+)(, ?([\d\.]+))?\)$/,hex3Rx=/^#([\da-fA-F])([\da-fA-F])([\da-fA-F])$/,hex4Rx=/^#([\da-fA-F])([\da-fA-F])([\da-fA-F])([\da-fA-F])$/,hex6Rx=/^#([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})$/,hex8Rx=/^#([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})$/,cssToRGB=new Map,[["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]],["orange",[255,165,0]],["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(e){cssToRGB.set(e[0],e[1])}),me.Color=me.Object.extend({init:function(e,t,i,n){return void 0===this.glArray&&(this.glArray=new Float32Array([0,0,0,1])),this.setColor(e,t,i,n)},setColor:function(e,t,i,n){return e instanceof me.Color?(this.glArray.set(e.glArray),e):(this.r=e,this.g=t,this.b=i,this.alpha=n,this)},clone:function(){return me.pool.pull("me.Color",this)},copy:function(e){return e instanceof me.Color?(this.glArray.set(e.glArray),this):this.parseCSS(e)},add:function(e){return this.glArray[0]=me.Math.clamp(this.glArray[0]+e.glArray[0],0,1),this.glArray[1]=me.Math.clamp(this.glArray[1]+e.glArray[1],0,1),this.glArray[2]=me.Math.clamp(this.glArray[2]+e.glArray[2],0,1),this.glArray[3]=(this.glArray[3]+e.glArray[3])/2,this},darken:function(e){return e=me.Math.clamp(e,0,1),this.glArray[0]*=e,this.glArray[1]*=e,this.glArray[2]*=e,this},lighten:function(e){return e=me.Math.clamp(e,0,1),this.glArray[0]=me.Math.clamp(this.glArray[0]+(1-this.glArray[0])*e,0,1),this.glArray[1]=me.Math.clamp(this.glArray[1]+(1-this.glArray[1])*e,0,1),this.glArray[2]=me.Math.clamp(this.glArray[2]+(1-this.glArray[2])*e,0,1),this},random:function(){return this.setColor(256*Math.random(),256*Math.random(),256*Math.random(),this.alpha)},equals:function(e){return this.glArray[0]===e.glArray[0]&&this.glArray[1]===e.glArray[1]&&this.glArray[2]===e.glArray[2]&&this.glArray[3]===e.glArray[3]},parseCSS:function(e){return cssToRGB.has(e)?this.setColor.apply(this,cssToRGB.get(e)):this.parseRGB(e)},parseRGB:function(e){var t=rgbaRx.exec(e);return t?this.setColor(+t[1],+t[2],+t[3],+t[5]):this.parseHex(e)},parseHex:function(e){var t;if(t=hex8Rx.exec(e))return this.setColor(parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16),(me.Math.clamp(parseInt(t[4],16),0,255)/255).toFixed(1));if(t=hex6Rx.exec(e))return this.setColor(parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16));if(t=hex4Rx.exec(e))return this.setColor(parseInt(t[1]+t[1],16),parseInt(t[2]+t[2],16),parseInt(t[3]+t[3],16),(me.Math.clamp(parseInt(t[4]+t[4],16),0,255)/255).toFixed(1));if(t=hex3Rx.exec(e))return this.setColor(parseInt(t[1]+t[1],16),parseInt(t[2]+t[2],16),parseInt(t[3]+t[3],16));throw new Error("invalid parameter: "+e)},toGL:function(){return this.glArray},toHex:function(){return"#"+_toHex(this.r)+_toHex(this.g)+_toHex(this.b)},toHex8:function(){return"#"+_toHex(this.r)+_toHex(this.g)+_toHex(this.b)+_toHex(255*this.alpha)},toRGB:function(){return"rgb("+this.r+","+this.g+","+this.b+")"},toRGBA:function(){return"rgba("+this.r+","+this.g+","+this.b+","+this.alpha+")"}}),Object.defineProperty(me.Color.prototype,"r",{get:function(){return~~(255*this.glArray[0])},set:function(e){this.glArray[0]=me.Math.clamp(~~e||0,0,255)/255},enumerable:!0,configurable:!0}),Object.defineProperty(me.Color.prototype,"g",{get:function(){return~~(255*this.glArray[1])},set:function(e){this.glArray[1]=me.Math.clamp(~~e||0,0,255)/255},enumerable:!0,configurable:!0}),Object.defineProperty(me.Color.prototype,"b",{get:function(){return~~(255*this.glArray[2])},set:function(e){this.glArray[2]=me.Math.clamp(~~e||0,0,255)/255},enumerable:!0,configurable:!0}),Object.defineProperty(me.Color.prototype,"alpha",{get:function(){return this.glArray[3]},set:function(e){this.glArray[3]=void 0===e?1:me.Math.clamp(+e,0,1)},enumerable:!0,configurable:!0}),me.save=function(){var e={};function t(e){return"add"===e||"remove"===e}var i={_init:function(){if(!0===me.device.localStorage){var t=localStorage.getItem("me.save");"string"==typeof t&&t.length>0&&(JSON.parse(t)||[]).forEach(function(t){e[t]=JSON.parse(localStorage.getItem("me.save."+t))})}},add:function(n){Object.keys(n).forEach(function(r){var s;t(r)||(s=r,Object.defineProperty(i,s,{configurable:!0,enumerable:!0,get:function(){return e[s]},set:function(t){e[s]=t,!0===me.device.localStorage&&localStorage.setItem("me.save."+s,JSON.stringify(t))}}),r in e||(i[r]=n[r]))}),!0===me.device.localStorage&&localStorage.setItem("me.save",JSON.stringify(Object.keys(e)))},remove:function(i){t(i)||void 0!==e[i]&&(delete e[i],!0===me.device.localStorage&&(localStorage.removeItem("me.save."+i),localStorage.setItem("me.save",JSON.stringify(Object.keys(e)))))}};return i}(),function(){me.TMXUtils=function(){var api={};function setTMXValue(name,type,value){var match;if("string"!=typeof value)return value;switch(type){case"int":case"float":value=Number(value);break;case"bool":value="true"===value;break;default:if(!value||me.utils.string.isBoolean(value))value=!value||"true"===value;else if(me.utils.string.isNumeric(value))value=Number(value);else if(0===value.search(/^json:/i)){match=value.split(/^json:/i)[1];try{value=JSON.parse(match)}catch(e){throw new Error("Unable to parse JSON: "+match)}}else if(0===value.search(/^eval:/i)){match=value.split(/^eval:/i)[1];try{value=eval(match)}catch(e){throw new Error("Unable to evaluate: "+match)}}else((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]);0===name.search(/^(ratio|anchorPoint)$/)&&"number"==typeof value&&(value={x:value,y:value})}return value}function parseAttributes(e,t){if(t.attributes&&t.attributes.length>0)for(var i=0;i=0;--n)o[i]+=s.charCodeAt(i*t+n)<<(n<<3);return o},api.decode=function(e,t,i){switch(i=i||"none",t=t||"none"){case"csv":return api.decodeCSV(e);case"base64":var n=api.decodeBase64AsArray(e,4);return"none"===i?n:api.decompress(n,i);case"none":return e;case"xml":throw new Error("XML encoding is deprecated, use base64 instead");default:throw new Error("Unknown layer encoding: "+t)}},api.normalize=function(e,t){var i=t.nodeName;switch(i){case"data":var n=api.parse(t);n.text=n.text||n.chunk.text,n.encoding=n.encoding||"xml",e.data=api.decode(n.text,n.encoding,n.compression),e.encoding="none";break;case"imagelayer":case"layer":case"objectgroup":case"group":var r=api.parse(t);r.type="layer"===i?"tilelayer":i,r.image&&(r.image=r.image.source),e.layers=e.layers||[],e.layers.push(r);break;case"animation":e.animation=api.parse(t).frames;break;case"frame":case"object":var s=i+"s";e[s]=e[s]||[],e[s].push(api.parse(t));break;case"tile":var o=api.parse(t);o.image&&(o.imagewidth=o.image.width,o.imageheight=o.image.height,o.image=o.image.source),e.tiles=e.tiles||{},e.tiles[o.id]=o;break;case"tileset":var a=api.parse(t);a.image&&(a.imagewidth=a.image.width,a.imageheight=a.image.height,a.image=a.image.source),e.tilesets=e.tilesets||[],e.tilesets.push(a);break;case"polygon":case"polyline":e[i]=[];for(var h,l=api.parse(t).points.split(" "),u=0;u0;var o=e.tileoffset;o&&(this.tileoffset.x=+o.x,this.tileoffset.y=+o.y);var a=e.tileproperties;if(a)for(t in a)a.hasOwnProperty(t)&&this.setTileProperty(+t+this.firstgid,a[t]);if(!1===this.isCollection){if(this.image=me.loader.getImage(e.image),!this.image)throw new Error("melonJS: '"+e.image+"' file for tileset '"+this.name+"' not found!");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();var h=+e.columns||~~(this.image.width/(this.tilewidth+this.spacing)),l=~~(this.image.height/(this.tileheight+this.spacing));this.lastgid=this.firstgid+(h*l-1||0),e.tilecount&&this.lastgid-this.firstgid+1!=+e.tilecount&&console.warn("Computed tilecount ("+(this.lastgid-this.firstgid+1)+") does not match expected tilecount ("+e.tilecount+")")}},getTileImage:function(e){return this.imageCollection[e]},setTileProperty:function(e,t){this.TileProperties[e]=t},contains:function(e){return e>=this.firstgid&&e<=this.lastgid},getViewTileId:function(e){return this.animations.has(e)?e=this.animations.get(e).cur.tileid:e-=this.firstgid,e},getTileProperties:function(e){return this.TileProperties[e]},update:function(e){var t=0,i=me.timer.getTime(),n=!1;return this._lastUpdate!==i&&(this._lastUpdate=i,this.animations.forEach(function(i){for(i.dt+=e,t=i.cur.duration;i.dt>=t;)i.dt-=t,i.idx=(i.idx+1)%i.frames.length,i.cur=i.frames[i.idx],t=i.cur.duration,n=!0})),n},drawTile:function(e,t,i,n){if(n.flipped&&(e.save(),e.translate(t,i),e.transform(n.currentTransform),t=i=0),!0===this.isCollection)e.drawImage(this.imageCollection[n.tileId],0,0,n.width,n.height,t,i,n.width,n.height);else{var r=this.atlas[this.getViewTileId(n.tileId)].offset;e.drawImage(this.image,r.x,r.y,this.tilewidth,this.tileheight,t,i,this.tilewidth+e.uvOffset,this.tileheight+e.uvOffset)}n.flipped&&e.restore()}}),me.TMXTilesetGroup=me.Object.extend({init:function(){this.tilesets=[],this.length=0},add:function(e){this.tilesets.push(e),this.length++},getTilesetByIndex:function(e){return this.tilesets[e]},getTilesetByGid:function(e){var t=-1;e&=536870911;for(var i=0,n=this.tilesets.length;i=this.tilesets[i].firstgid&&(t=i)}if(-1!==t)return this.tilesets[t];throw new Error("no matching tileset found for gid "+e)}}),offsetsStaggerX=[{x:0,y:0},{x:1,y:-1},{x:1,y:0},{x:2,y:0}],offsetsStaggerY=[{x:0,y:0},{x:-1,y:1},{x:0,y:1},{x:0,y:2}],me.TMXRenderer=me.Object.extend({init:function(e,t,i,n){this.cols=e,this.rows=t,this.tilewidth=i,this.tileheight=n},canRender:function(e){return this.cols===e.cols&&this.rows===e.rows&&this.tilewidth===e.tilewidth&&this.tileheight===e.tileheight},pixelToTileCoords:function(e,t,i){return i},tileToPixelCoords:function(e,t,i){return i},drawTile:function(e,t,i,n){},drawTileLayer:function(e,t,i){}}),me.TMXOrthogonalRenderer=me.TMXRenderer.extend({canRender:function(e){return"orthogonal"===e.orientation&&this._super(me.TMXRenderer,"canRender",[e])},pixelToTileCoords:function(e,t,i){return(i||new me.Vector2d).set(e/this.tilewidth,t/this.tileheight)},tileToPixelCoords:function(e,t,i){return(i||new me.Vector2d).set(e*this.tilewidth,t*this.tileheight)},adjustPosition:function(e){"number"==typeof e.gid&&(e.y-=e.height)},drawTile:function(e,t,i,n){var r=n.tileset;r.drawTile(e,r.tileoffset.x+t*this.tilewidth,r.tileoffset.y+(i+1)*this.tileheight-r.tileheight,n)},drawTileLayer:function(e,t,i){var n=1,r=1,s=this.pixelToTileCoords(Math.max(i.pos.x-(t.maxTileSize.width-t.tilewidth),0),Math.max(i.pos.y-(t.maxTileSize.height-t.tileheight),0),me.pool.pull("me.Vector2d")).floorSelf(),o=this.pixelToTileCoords(i.pos.x+i.width+this.tilewidth,i.pos.y+i.height+this.tileheight,me.pool.pull("me.Vector2d")).ceilSelf();switch(o.x=o.x>this.cols?this.cols:o.x,o.y=o.y>this.rows?this.rows:o.y,t.renderorder){case"right-up":o.y=s.y+(s.y=o.y)-o.y,r=-1;break;case"left-down":o.x=s.x+(s.x=o.x)-o.x,n=-1;break;case"left-up":o.x=s.x+(s.x=o.x)-o.x,o.y=s.y+(s.y=o.y)-o.y,n=-1,r=-1}for(var a=s.y;a!==o.y;a+=r)for(var h=s.x;h!==o.x;h+=n){var l=t.layerData[h][a];l&&this.drawTile(e,h,a,l)}me.pool.push(s),me.pool.push(o)}}),me.TMXIsometricRenderer=me.TMXRenderer.extend({init:function(e,t,i,n){this._super(me.TMXRenderer,"init",[e,t,i,n]),this.hTilewidth=i/2,this.hTileheight=n/2,this.originX=this.rows*this.hTilewidth},canRender:function(e){return"isometric"===e.orientation&&this._super(me.TMXRenderer,"canRender",[e])},pixelToTileCoords:function(e,t,i){return(i||new me.Vector2d).set(t/this.tileheight+(e-this.originX)/this.tilewidth,t/this.tileheight-(e-this.originX)/this.tilewidth)},tileToPixelCoords:function(e,t,i){return(i||new me.Vector2d).set((e-t)*this.hTilewidth+this.originX,(e+t)*this.hTileheight)},adjustPosition:function(e){var t=e.x/this.hTilewidth,i=e.y/this.tileheight,n=me.pool.pull("me.Vector2d");this.tileToPixelCoords(t,i,n),e.x=n.x,e.y=n.y,me.pool.push(n)},drawTile:function(e,t,i,n){var r=n.tileset;r.drawTile(e,(this.cols-1)*r.tilewidth+(t-i)*r.tilewidth>>1,-r.tilewidth+(t+i)*r.tileheight>>2,n)},drawTileLayer:function(e,t,i){var n=t.tileset,r=this.pixelToTileCoords(i.pos.x-n.tilewidth,i.pos.y-n.tileheight,me.pool.pull("me.Vector2d")).floorSelf(),s=this.pixelToTileCoords(i.pos.x+i.width+n.tilewidth,i.pos.y+i.height+n.tileheight,me.pool.pull("me.Vector2d")).ceilSelf(),o=this.tileToPixelCoords(s.x,s.y,me.pool.pull("me.Vector2d")),a=this.tileToPixelCoords(r.x,r.y,me.pool.pull("me.Vector2d"));a.x-=this.hTilewidth,a.y+=this.tileheight;var h=a.y-i.pos.y>this.hTileheight,l=i.pos.x-a.x=0&&c.y>=0&&c.xthis.cols?this.cols:r.x,r.y=r.y>this.rows?this.rows:r.y;for(var s=n.y;s0,this.isAnimated&&(this.preRender=!1),this.getBounds().resize(this.width,this.height)},onDeactivateEvent:function(){this.animatedTilesets=void 0},setRenderer:function(e){this.renderer=e},getRenderer:function(e){return this.renderer},getTileId:function(e,t){var i=this.getTile(e,t);return i?i.tileId:null},getTile:function(e,t){if(this.containsPoint(e,t)){var i=this.renderer,n=null,r=i.pixelToTileCoords(e,t,me.pool.pull("me.Vector2d"));r.x>=0&&r.x=0&&r.y0&&(t.autoSort=!0,t.autoDepth=!0,i.push(t))}return i},getLayers:function(){return this.readMapObjects(this.data),this.layers},destroy:function(){this.tilesets=void 0,this.layers.length=0,this.objectGroups.length=0,this.initialized=!1}})}(),me.levelDirector=function(){var e={},t={},i=[],n=0,r=null;function s(s,o,a){o.container.reset(),me.game.reset(),t[e.getCurrentLevelId()]&&t[e.getCurrentLevelId()].destroy(),n=i.indexOf(s),function(e,i,n,s){var o=t[e],a=i.autoSort;function h(){i.pos.set(Math.max(0,~~((me.game.viewport.width-o.width)/2)),Math.max(0,~~((me.game.viewport.height-o.height)/2)),0)}i.autoSort=!1,s&&me.game.viewport.setBounds(0,0,Math.max(o.width,me.game.viewport.width),Math.max(o.height,me.game.viewport.height)),me.utils.resetGUID(e,o.nextobjectid),i.anchorPoint.set(0,0),o.addTo(i,n),i.sort(!0),i.autoSort=a,i.resize(o.width,o.height),s&&(h(),r&&me.event.unsubscribe(r),r=me.event.subscribe(me.event.VIEWPORT_ONRESIZE,h))}(s,o.container,o.flatten,o.setViewportBounds),me.event.publish(me.event.LEVEL_LOADED,[s]),o.onLoaded(s),a&&me.state.restart()}return e.reset=function(){},e.addLevel=function(){throw new Error("no level loader defined")},e.addTMXLevel=function(e,n){return null==t[e]&&(t[e]=new me.TMXTileMap(e,me.loader.getTMX(e)),i.push(e),n&&n(),!0)},e.loadLevel=function(e,i){if(i=Object.assign({container:me.game.world,onLoaded:me.game.onLevelLoaded,flatten:me.game.mergeGroup,setViewportBounds:!0},i||{}),void 0===t[e])throw new Error("level "+e+" not found");if(!(t[e]instanceof me.TMXTileMap))throw new Error("no level loader defined");return me.state.isRunning()?(me.state.stop(),me.utils.function.defer(s,this,e,i,!0)):s(e,i),!0},e.getCurrentLevelId=function(){return i[n]},e.getCurrentLevel=function(){return t[e.getCurrentLevelId()]},e.reloadLevel=function(t){return e.loadLevel(e.getCurrentLevelId(),t)},e.nextLevel=function(t){return n+1=0&&e.loadLevel(i[n-1],t)},e.levelCount=function(){return i.length},e}(),me.Tween=function(e){var t=null,i=null,n=null,r=null,s=null,o=null,a=null,h=null,l=null,u=null,c=null,d=null,f=null,p=null,m=null,g=null,v=null;this.isRenderable=!1,this._resumeCallback=function(e){l&&(l+=e)},this.setProperties=function(e){for(var y in t=e,i={},n={},r={},s=1e3,o=0,a=!1,h=0,l=null,u=me.Tween.Easing.Linear.None,c=me.Tween.Interpolation.Linear,d=[],f=null,p=!1,m=null,g=null,v=me.timer.lastUpdate,this.isPersistent=!1,this.updateWhenPaused=!1,e)"object"!==_typeof(e)&&(i[y]=parseFloat(e[y]))},this.setProperties(e),this.onResetEvent=function(e){this.setProperties(e)},this.onActivateEvent=function(){me.event.subscribe(me.event.STATE_RESUME,this._resumeCallback)},this.onDeactivateEvent=function(){me.event.unsubscribe(me.event.STATE_RESUME,this._resumeCallback)},this.to=function(e,t){return void 0!==t&&(s=t),n=e,this},this.start=function(e){for(var s in p=!1,me.game.world.addChild(this),l=(void 0===e?me.timer.getTime():e)+h,n){if(n[s]instanceof Array){if(0===n[s].length)continue;n[s]=[t[s]].concat(n[s])}i[s]=t[s],i[s]instanceof Array==0&&(i[s]*=1),r[s]=i[s]||0}return this},this.stop=function(){return me.game.world.removeChildNow(this),this},this.delay=function(e){return h=e,this},this.repeat=function(e){return o=e,this},this.yoyo=function(e){return a=e,this},this.easing=function(e){if("function"!=typeof e)throw new Error("invalid easing function for me.Tween.easing()");return u=e,this},this.interpolation=function(e){return c=e,this},this.chain=function(){return d=arguments,this},this.onStart=function(e){return f=e,this},this.onUpdate=function(e){return m=e,this},this.onComplete=function(e){return g=e,this},this.update=function(e){var y,_=v=me.timer.lastUpdate>v?me.timer.lastUpdate:v+e;if(_1?1:x);for(y in n){var b=i[y]||0,T=n[y];T instanceof Array?t[y]=c(T,w):("string"==typeof T&&(T=b+parseFloat(T)),"number"==typeof T&&(t[y]=b+(T-b)*w))}if(null!==m&&m.call(t,w),1===x){if(o>0){for(y in isFinite(o)&&o--,r){if("string"==typeof n[y]&&(r[y]=r[y]+parseFloat(n[y])),a){var E=r[y];r[y]=n[y],n[y]=E}i[y]=r[y]}return l=_+h,!0}me.game.world.removeChildNow(this),null!==g&&g.call(t);for(var A=0,S=d.length;A1?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[i1;t--)i*=t;return a[e]=i}),CatmullRom:function(e,t,i,n,r){var s=.5*(i-e),o=.5*(n-t),a=r*r;return(2*t-2*i+s+o)*(r*a)+(-3*t+3*i-2*s-o)*a+s*r+t}}},me.plugins={},me.plugin=((singleton={}).Base=me.Object.extend({init:function(){this.version="7.0.0"}}),singleton.patch=function(e,t,i){if(void 0!==e.prototype&&(e=e.prototype),"function"!=typeof e[t])throw new Error(t+" is not an existing function");var n=e[t];Object.defineProperty(e,t,{configurable:!0,value:function(e,t){return function(){this._patched=n;var e=t.apply(this,arguments);return this._patched=null,e}}(0,i)})},singleton.register=function(e,t){if(me.plugin[t])throw new Error("plugin "+t+" already registered");var i=[];arguments.length>2&&(i=Array.prototype.slice.call(arguments,1)),i[0]=e;var n=new(e.bind.apply(e,i));if(!(n&&n instanceof me.plugin.Base))throw new Error("Plugin should extend the me.plugin.Base Class !");if(me.sys.checkVersion(n.version)>0)throw new Error("Plugin version mismatch, expected: "+n.version+", got: "+me.version);me.plugins[t]=n},singleton),me.DraggableEntity=(Entity=me.Entity,Input=me.input,Event=me.event,Vector=me.Vector2d,Entity.extend({init:function(e,t,i){this._super(Entity,"init",[e,t,i]),this.dragging=!1,this.dragId=null,this.grabOffset=new Vector(0,0),this.onPointerEvent=Input.registerPointerEvent,this.removePointerEvent=Input.releasePointerEvent,this.initEvents()},initEvents:function(){var e=this;this.mouseDown=function(e){this.translatePointerEvent(e,Event.DRAGSTART)},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(t,i){i===e&&e.dragStart(t)}),Event.subscribe(Event.DRAGEND,function(t,i){i===e&&e.dragEnd(t)})},translatePointerEvent:function(e,t){Event.publish(t,[e,this])},dragStart:function(e){if(!1===this.dragging)return this.dragging=!0,this.grabOffset.set(e.gameX,e.gameY),this.grabOffset.sub(this.pos),!1},dragMove:function(e){!0===this.dragging&&(this.pos.set(e.gameX,e.gameY,this.pos.z),this.pos.sub(this.grabOffset))},dragEnd:function(){if(!0===this.dragging)return this.dragging=!1,!1},destroy:function(){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.DroptargetEntity=function(e,t){return e.extend({init:function(i,n,r){this.CHECKMETHOD_OVERLAP="overlaps",this.CHECKMETHOD_CONTAINS="contains",this.checkMethod=null,this._super(e,"init",[i,n,r]),t.subscribe(t.DRAGEND,this.checkOnMe.bind(this)),this.checkMethod=this[this.CHECKMETHOD_OVERLAP]},setCheckMethod:function(e){void 0!==this[e]&&(this.checkMethod=this[e])},checkOnMe:function(e,t){t&&this.checkMethod(t.getBounds())&&this.drop(t)},drop:function(){},destroy:function(){t.unsubscribe(t.DRAGEND,this.checkOnMe)}})}(me.Entity,me.event),me.CollectableEntity=me.Entity.extend({init:function(e,t,i){this._super(me.Entity,"init",[e,t,i]),this.body.collisionType=me.collision.types.COLLECTABLE_OBJECT}}),me.LevelEntity=me.Entity.extend({init:function(e,t,i){this._super(me.Entity,"init",[e,t,i]),this.nextlevel=i.to,this.fade=i.fade,this.duration=i.duration,this.fading=!1,this.name="levelEntity",this.gotolevel=i.to,this.loadLevelSettings={},["container","onLoaded","flatten","setViewportBounds"].forEach(function(e){void 0!==i[e]&&(this.loadLevelSettings[e]=i[e])}.bind(this)),this.body.collisionType=me.collision.types.ACTION_OBJECT},getlevelSettings:function(){return"string"==typeof this.loadLevelSettings.container&&(this.loadLevelSettings.container=me.game.world.getChildByName(this.loadLevelSettings.container)[0]),this.loadLevelSettings},onFadeComplete:function(){me.levelDirector.loadLevel(this.gotolevel,this.getlevelSettings()),me.game.viewport.fadeOut(this.fade,this.duration)},goTo:function(e){this.gotolevel=e||this.nextlevel,this.fade&&this.duration?this.fading||(this.fading=!0,me.game.viewport.fadeIn(this.fade,this.duration,this.onFadeComplete.bind(this))):me.levelDirector.loadLevel(this.gotolevel,this.getlevelSettings())},onCollision:function(){return"levelEntity"===this.name&&this.goTo.apply(this),!1}}),canvas=me.video.createCanvas(1,1),(context=canvas.getContext("2d")).fillStyle="#fff",context.fillRect(0,0,1,1),pixel=canvas,me.ParticleEmitterSettings={width:0,height:0,image:pixel,totalParticles:50,angle:Math.PI/2,angleVariation:0,minLife:1e3,maxLife:3e3,speed:2,speedVariation:1,minRotation:0,maxRotation:0,minStartScale:1,maxStartScale:1,minEndScale:0,maxEndScale:0,gravity:0,wind:0,followTrajectory:!1,textureAdditive:!1,onlyInViewport:!0,floating:!1,maxParticles:10,frequency:100,duration:1/0,framesToSkip:0},me.ParticleEmitter=me.Rect.extend({init:function(e,t,i){this._stream=!1,this._frequencyTimer=0,this._durationTimer=0,this._enabled=!1,this.isRenderable=!1,this._super(me.Rect,"init",[e,t,1/0,1/0]),this.autoSort=!1,this.container=new me.ParticleContainer(this),Object.defineProperty(this.pos,"z",{get:function(){return this.container.pos.z}.bind(this),set:function(e){this.container.pos.z=e}.bind(this),enumerable:!0,configurable:!0}),Object.defineProperty(this,"floating",{get:function(){return this.container.floating},set:function(e){this.container.floating=e},enumerable:!0,configurable:!0}),this.reset(i)},onActivateEvent:function(){this.ancestor.addChild(this.container),this.container.pos.z=this.pos.z,this.ancestor.autoSort||this.ancestor.sort()},onDeactivateEvent:function(){this.ancestor.hasChild(this.container)&&this.ancestor.removeChildNow(this.container)},destroy:function(){this.reset()},getRandomPointX:function(){return this.pos.x+me.Math.randomFloat(0,this.width)},getRandomPointY:function(){return this.pos.y+me.Math.randomFloat(0,this.height)},reset:function(e){e=e||{};var t=me.ParticleEmitterSettings,i="number"==typeof e.width?e.width:t.width,n="number"==typeof e.height?e.height:t.height;this.resize(i,n),Object.assign(this,t,e),this.container.reset()},addParticles:function(e){for(var t=0;t<~~e;t++){var i=me.pool.pull("me.Particle",this);this.container.addChild(i)}},isRunning:function(){return this._enabled&&this._stream},streamParticles:function(e){this._enabled=!0,this._stream=!0,this.frequency=Math.max(this.frequency,1),this._durationTimer="number"==typeof e?e:this.duration},stopStream:function(){this._enabled=!1},burstParticles:function(e){this._enabled=!0,this._stream=!1,this.addParticles("number"==typeof e?e:this.totalParticles),this._enabled=!1},update:function(e){if(this._enabled&&this._stream){if(this._durationTimer!==1/0&&(this._durationTimer-=e,this._durationTimer<=0))return this.stopStream(),!1;this._frequencyTimer+=e;var t=this.container.children.length;t=this.frequency&&(t+this.maxParticles<=this.totalParticles?this.addParticles(this.maxParticles):this.addParticles(this.totalParticles-t),this._frequencyTimer=0)}return!0}}),me.ParticleContainer=me.Container.extend({init:function(e){this._super(me.Container,"init",[me.game.viewport.pos.x,me.game.viewport.pos.y,me.game.viewport.width,me.game.viewport.height]),this.autoSort=!1,this._updateCount=0,this._dt=0,this._emitter=e,this.autoTransform=!1,this.anchorPoint.set(0,0),this.isKinematic=!0},update:function(e){if(++this._updateCount>this._emitter.framesToSkip&&(this._updateCount=0),this._updateCount>0)return this._dt+=e,!1;e+=this._dt,this._dt=0;for(var t=me.game.viewport,i=this.children.length-1;i>=0;--i){var n=this.children[i];n.inViewport=t.isVisible(n,this.floating),n.update(e)||this.removeChildNow(n)}return!0},draw:function(e,t){if(this.children.length>0){var i,n=e.getContext();this._emitter.textureAdditive&&(i=n.globalCompositeOperation,n.globalCompositeOperation="lighter"),this._super(me.Container,"draw",[e,t]),this._emitter.textureAdditive&&(n.globalCompositeOperation=i)}}}),me.Particle=me.Renderable.extend({init:function(e){this._super(me.Renderable,"init",[e.getRandomPointX(),e.getRandomPointY(),e.image.width,e.image.height]),this.alwaysUpdate=!0,this.image=e.image;var t=e.angle+(e.angleVariation>0?(me.Math.randomFloat(0,2)-1)*e.angleVariation:0),i=e.speed+(e.speedVariation>0?(me.Math.randomFloat(0,2)-1)*e.speedVariation:0);this.vel=new me.Vector2d(i*Math.cos(t),-i*Math.sin(t)),this.life=me.Math.randomFloat(e.minLife,e.maxLife),this.startLife=this.life,this.startScale=me.Math.clamp(me.Math.randomFloat(e.minStartScale,e.maxStartScale),e.minStartScale,e.maxStartScale),this.endScale=me.Math.clamp(me.Math.randomFloat(e.minEndScale,e.maxEndScale),e.minEndScale,e.maxEndScale),this.gravity=e.gravity,this.wind=e.wind,this.followTrajectory=e.followTrajectory,this.onlyInViewport=e.onlyInViewport,this.pos.z=e.z,this._deltaInv=me.sys.fps/1e3,e.followTrajectory||(this.angle=me.Math.randomFloat(e.minRotation,e.maxRotation))},update:function(e){var t=e*this._deltaInv;this.life=this.life>e?this.life-e:0;var i=this.life/this.startLife,n=this.startScale;this.startScale>this.endScale?n=(n*=i)this.endScale?this.endScale:n),this.alpha=i,this.vel.x+=this.wind*t,this.vel.y+=this.gravity*t;var r=this.followTrajectory?Math.atan2(this.vel.y,this.vel.x):this.angle;return this.pos.x+=this.vel.x*t,this.pos.y+=this.vel.y*t,this.currentTransform.setTransform(n,0,0,0,n,0,this.pos.x,this.pos.y,1).rotate(r),(this.inViewport||!this.onlyInViewport)&&this.life>0},preDraw:function(e){e.save(),e.setGlobalAlpha(e.globalAlpha()*this.alpha),e.transform(this.currentTransform)},draw:function(e){var t=this.width,i=this.height;e.drawImage(this.image,0,0,t,i,-t/2,-i/2,t,i)}}),me.ScreenObject=me.Stage.extend({init:function(e){this._super(me.Stage,"init",e),console.log("me.ScreenObject is deprecated, please use me.Stage")}}),me.Font=me.Text.extend({init:function(e,t,i,n){var r={font:e,size:t,fillStyle:i,textAlign:n};this._super(me.Text,"init",[0,0,r]),console.log("me.Font is deprecated, please use me.Text")},setFont:function(e,t,i,n){return void 0!==i&&this.fillStyle.copy(i),void 0!==n&&(this.textAlign=n),this._super(me.Text,"setFont",[e,t])}}),me.BitmapFontData=me.BitmapTextData,me.BitmapFont=me.BitmapText.extend({init:function(e,t,i,n,r){var s={font:t,fontData:e,size:i,textAlign:n,textBaseline:r};this._super(me.BitmapText,"init",[0,0,s]),console.log("me.BitmapFont is deprecated, please use me.BitmapText")}}),me.Renderer.prototype.drawShape=function(){console.log("drawShape() is deprecated, please use the stroke() or fill() function"),me.Renderer.prototype.stroke.apply(this,arguments)},me.CanvasRenderer.prototype.Texture=me.Renderer.prototype.Texture,me.WebGLRenderer.prototype.Texture=me.Renderer.prototype.Texture,me.video.getPos=function(){return console.log("me.video.getPos() is deprecated, please use me.video.renderer.getBounds()"),me.video.renderer.getBounds()},me.Error=me.Object.extend.bind(Error)({init:function(e){this.name="me.Error",this.message=e}})}(); \ No newline at end of file