/* * Copyright 2012 Priit Kallas * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ (function(exports) { 'use strict'; /** * A null function - does nothing, returns nothing */ var nullFunction = function() {}; /** * The null platform, which doesn't support anything */ var nullPlatform = { getType: function() { return 'null'; }, isSupported: function() { return false; }, update: nullFunction }; /** * This strategy uses a timer function to call an update function. * The timer (re)start function can be provided or the strategy reverts to * one of the window.*requestAnimationFrame variants. * * @class AnimFrameUpdateStrategy * @constructor * @param {Function} [requestAnimationFrame] function to use for timer creation * @module Gamepad */ var AnimFrameUpdateStrategy = function(requestAnimationFrame) { var that = this; var win = window; this.update = nullFunction; this.requestAnimationFrame = requestAnimationFrame || win.requestAnimationFrame || win.webkitRequestAnimationFrame || win.mozRequestAnimationFrame; /** * This method calls the (user) update and restarts itself * @method tickFunction */ this.tickFunction = function() { that.update(); that.startTicker(); }; /** * (Re)Starts the ticker * @method startTicker */ this.startTicker = function() { that.requestAnimationFrame.apply(win, [that.tickFunction]); }; }; /** * Starts the update strategy using the given function * * @method start * @param {Function} updateFunction the function to call at each update */ AnimFrameUpdateStrategy.prototype.start = function(updateFunction) { this.update = updateFunction || nullFunction; this.startTicker(); }; /** * This strategy gives the user the ability to call the library internal * update function on request. Use this strategy if you already have a * timer function running by requestAnimationFrame and you need fine control * over when the gamepads are updated. * * @class ManualUpdateStrategy * @constructor * @module Gamepad */ var ManualUpdateStrategy = function() { }; /** * Calls the update function in the started state. Does nothing otherwise. * @method update */ ManualUpdateStrategy.prototype.update = nullFunction; /** * Starts the update strategy using the given function * * @method start * @param {Function} updateFunction the function to call at each update */ ManualUpdateStrategy.prototype.start = function(updateFunction) { this.update = updateFunction || nullFunction; }; /** * This platform is for webkit based environments that need to be polled * for updates. * * @class WebKitPlatform * @constructor * @param {Object} listener the listener to provide _connect and _disconnect callbacks * @param {Function} gamepadGetter the poll function to return an array of connected gamepads * @module Gamepad */ var WebKitPlatform = function(listener, gamepadGetter) { this.listener = listener; this.gamepadGetter = gamepadGetter; this.knownGamepads = []; }; /** * Provides a platform object that returns true for is isSupported() if valid. * @method factory * @static * @param {Object} listener the listener to use * @return {Object} a platform object */ WebKitPlatform.factory = function(listener) { var platform = nullPlatform; var navigator = window && window.navigator; if (navigator) { if (typeof(navigator.webkitGamepads) !== 'undefined') { platform = new WebKitPlatform(listener, function() { return navigator.webkitGamepads; }); } else if (typeof(navigator.webkitGetGamepads) !== 'undefined') { platform = new WebKitPlatform(listener, function() { return navigator.webkitGetGamepads(); }); } } return platform; }; /** * @method getType() * @static * @return {String} 'WebKit' */ WebKitPlatform.getType = function() { return 'WebKit'; }, /** * @method getType() * @return {String} 'WebKit' */ WebKitPlatform.prototype.getType = function() { return WebKitPlatform.getType(); }, /** * @method isSupported * @return {Boolean} true */ WebKitPlatform.prototype.isSupported = function() { return true; }; /** * Queries the currently connected gamepads and reports any changes. * @method update */ WebKitPlatform.prototype.update = function() { var that = this; var gamepads = Array.prototype.slice.call(this.gamepadGetter(), 0); var gamepad; var i; for (i = this.knownGamepads.length - 1; i >= 0; i--) { gamepad = this.knownGamepads[i]; if (gamepads.indexOf(gamepad) < 0) { this.knownGamepads.splice(i, 1); this.listener._disconnect(gamepad); } } for (i = 0; i < gamepads.length; i++) { gamepad = gamepads[i]; if (gamepad && (that.knownGamepads.indexOf(gamepad) < 0)) { that.knownGamepads.push(gamepad); that.listener._connect(gamepad); } } }; /** * This platform is for mozilla based environments that provide gamepad * updates via events. * * @class FirefoxPlatform * @constructor * @module Gamepad */ var FirefoxPlatform = function(listener) { this.listener = listener; window.addEventListener('gamepadconnected', function(e) { listener._connect(e.gamepad); }); window.addEventListener('gamepaddisconnected', function(e) { listener._disconnect(e.gamepad); }); }; /** * Provides a platform object that returns true for is isSupported() if valid. * @method factory * @static * @param {Object} listener the listener to use * @return {Object} a platform object */ FirefoxPlatform.factory = function(listener) { var platform = nullPlatform; if (window && (typeof(window.addEventListener) !== 'undefined')) { platform = new FirefoxPlatform(listener); } return platform; }; /** * @method getType() * @static * @return {String} 'Firefox' */ FirefoxPlatform.getType = function() { return 'Firefox'; }, /** * @method getType() * @return {String} 'Firefox' */ FirefoxPlatform.prototype.getType = function() { return FirefoxPlatform.getType(); }, /** * @method isSupported * @return {Boolean} true */ FirefoxPlatform.prototype.isSupported = function() { return true; }; /** * Does nothing on the Firefox platform * @method update */ FirefoxPlatform.prototype.update = nullFunction; /** * Provides simple interface and multi-platform support for the gamepad API. * * You can change the deadzone and maximizeThreshold parameters to suit your * taste but the defaults should generally work fine. * * @class Gamepad * @constructor * @param {Object} [updateStrategy] an update strategy, defaulting to * {{#crossLink "AnimFrameUpdateStrategy"}}{{/crossLink}} * @module Gamepad * @author Priit Kallas */ var Gamepad = function(updateStrategy) { this.updateStrategy = updateStrategy || new AnimFrameUpdateStrategy(); this.gamepads = []; this.listeners = {}; this.platform = nullPlatform; this.deadzone = 0.03; this.maximizeThreshold = 0.97; }; /** * The available update strategies * @property UpdateStrategies * @param {AnimFrameUpdateStrategy} AnimFrameUpdateStrategy * @param {ManualUpdateStrategy} ManualUpdateStrategy */ Gamepad.UpdateStrategies = { AnimFrameUpdateStrategy: AnimFrameUpdateStrategy, ManualUpdateStrategy: ManualUpdateStrategy }; /** * List of factories of supported platforms. Currently available platforms: * {{#crossLink "WebKitPlatform"}}{{/crossLink}}, * {{#crossLink "FirefoxPlatform"}}{{/crossLink}}, * @property PlatformFactories * @type {Array} */ Gamepad.PlatformFactories = [WebKitPlatform.factory, FirefoxPlatform.factory]; /** * List of supported controller types. * * @property Type * @param {String} Type.PLAYSTATION Playstation controller * @param {String} Type.LOGITECH Logitech controller * @param {String} Type.XBOX XBOX controller * @param {String} Type.UNKNOWN Unknown controller */ Gamepad.Type = { PLAYSTATION: 'playstation', LOGITECH: 'logitech', XBOX: 'xbox', UNKNOWN: 'unknown' }; /* * List of events you can expect from the library. * * CONNECTED, DISCONNECTED and UNSUPPORTED events include the gamepad in * question and tick provides the list of all connected gamepads. * * BUTTON_DOWN and BUTTON_UP events provide an alternative to polling button states at each tick. * * AXIS_CHANGED is called if a value of some specific axis changes. */ Gamepad.Event = { /** * Triggered when a new controller connects. * * @event connected * @param {Object} device */ CONNECTED: 'connected', /** * Called when an unsupported controller connects. * * @event unsupported * @param {Object} device * @deprecated not used anymore. Any controller is supported. */ UNSUPPORTED: 'unsupported', /** * Triggered when a controller disconnects. * * @event disconnected * @param {Object} device */ DISCONNECTED: 'disconnected', /** * Called regularly with the latest controllers info. * * @event tick * @param {Array} gamepads */ TICK: 'tick', /** * Called when a gamepad button is pressed down. * * @event button-down * @param {Object} event * @param {Object} event.gamepad The gamepad object * @param {String} event.control Control name */ BUTTON_DOWN: 'button-down', /** * Called when a gamepad button is released. * * @event button-up * @param {Object} event * @param {Object} event.gamepad The gamepad object * @param {String} event.control Control name */ BUTTON_UP: 'button-up', /** * Called when gamepad axis value changed. * * @event axis-changed * @param {Object} event * @param {Object} event.gamepad The gamepad object * @param {String} event.axis Axis name * @param {Number} event.value New axis value */ AXIS_CHANGED: 'axis-changed' }; /** * List of standard button names. The index is the according standard button * index as per standard. * * @property StandardButtons */ Gamepad.StandardButtons = [ 'FACE_1', 'FACE_2', 'FACE_3', 'FACE_4', 'LEFT_TOP_SHOULDER', 'RIGHT_TOP_SHOULDER', 'LEFT_BOTTOM_SHOULDER', 'RIGHT_BOTTOM_SHOULDER', 'SELECT_BACK', 'START_FORWARD', 'LEFT_STICK', 'RIGHT_STICK', 'DPAD_UP', 'DPAD_DOWN', 'DPAD_LEFT', 'DPAD_RIGHT', 'HOME' ]; /** * List of standard axis names. The index is the according standard axis * index as per standard. * * @property StandardAxes */ Gamepad.StandardAxes = ['LEFT_STICK_X', 'LEFT_STICK_Y', 'RIGHT_STICK_X', 'RIGHT_STICK_Y']; var getControlName = function(names, index, extraPrefix) { return (index < names.length) ? names[index] : extraPrefix + (index - names.length + 1); }; /** * The standard mapping that represents the mapping as per definition. * Each button and axis map to the same index. * * @property StandardMapping */ Gamepad.StandardMapping = { env: {}, buttons: { byButton: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] }, axes: { byAxis: [0, 1, 2, 3] } }; /** * Mapping of various gamepads that differ from the standard mapping on * different platforms too unify their buttons and axes. * * Each mapping should have an 'env' object, which describes the environment * in which the mapping is active. The more entries such an environment has, * the more specific it is. * * Mappings are expressed for both buttons and axes. Buttons might refer to * axes if they are notified as such. * * @property Mappings */ Gamepad.Mappings = [ // PS3 controller on Firefox { env: { platform: FirefoxPlatform.getType(), type: Gamepad.Type.PLAYSTATION }, buttons: { byButton: [14, 13, 15, 12, 10, 11, 8, 9, 0, 3, 1, 2, 4, 6, 7, 5, 16] }, axes: { byAxis: [0, 1, 2, 3] } }, // Logitech gamepad on WebKit { env: { platform: WebKitPlatform.getType(), type: Gamepad.Type.LOGITECH }, buttons: { // TODO: This can't be right - LEFT/RIGHT_STICK have same mappings as HOME/DPAD_UP byButton: [1, 2, 0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 11, 12, 13, 14, 10] }, axes: { byAxis: [0, 1, 2, 3] } }, // Logitech gamepad on Firefox { env: { platform: FirefoxPlatform.getType(), type: Gamepad.Type.LOGITECH }, buttons: { byButton: [0, 1, 2, 3, 4, 5, -1, -1, 6, 7, 8, 9, 11, 12, 13, 14, 10], byAxis: [-1, -1, -1, -1, -1, -1, [2, 0, 1], [2, 0, -1] ] }, axes: { byAxis: [0, 1, 3, 4] } } ]; /** * Initializes the gamepad. * * You usually want to bind to the events first and then initialize it. * * @method init * @return {Boolean} true if a supporting platform was detected, false otherwise. */ Gamepad.prototype.init = function() { var platform = Gamepad.resolvePlatform(this); var that = this; this.platform = platform; this.updateStrategy.start(function() { that._update(); }); return platform.isSupported(); }; /** * Binds a listener to a gamepad event. * * @method bind * @param {String} event Event to bind to, one of Gamepad.Event.. * @param {Function} listener Listener to call when given event occurs * @return {Gamepad} Self */ Gamepad.prototype.bind = function(event, listener) { if (typeof(this.listeners[event]) === 'undefined') { this.listeners[event] = []; } this.listeners[event].push(listener); return this; }; /** * Removes listener of given type. * * If no type is given, all listeners are removed. If no listener is given, all * listeners of given type are removed. * * @method unbind * @param {String} [type] Type of listener to remove * @param {Function} [listener] The listener function to remove * @return {Boolean} Was unbinding the listener successful */ Gamepad.prototype.unbind = function(type, listener) { if (typeof(type) === 'undefined') { this.listeners = {}; return; } if (typeof(listener) === 'undefined') { this.listeners[type] = []; return; } if (typeof(this.listeners[type]) === 'undefined') { return false; } for (var i = 0; i < this.listeners[type].length; i++) { if (this.listeners[type][i] === listener) { this.listeners[type].splice(i, 1); return true; } } return false; }; /** * Returns the number of connected gamepads. * * @method count * @return {Number} */ Gamepad.prototype.count = function() { return this.gamepads.length; }; /** * Fires an internal event with given data. * * @method _fire * @param {String} event Event to fire, one of Gamepad.Event.. * @param {*} data Data to pass to the listener * @private */ Gamepad.prototype._fire = function(event, data) { if (typeof(this.listeners[event]) === 'undefined') { return; } for (var i = 0; i < this.listeners[event].length; i++) { this.listeners[event][i].apply(this.listeners[event][i], [data]); } }; /** * @method getNullPlatform * @static * @return {Object} a platform that does not support anything */ Gamepad.getNullPlatform = function() { return Object.create(nullPlatform); }; /** * Resolves platform. * * @method resolvePlatform * @static * @param listener {Object} the listener to handle _connect() or _disconnect() calls * @return {Object} A platform instance */ Gamepad.resolvePlatform = function(listener) { var platform = nullPlatform; var i; for (i = 0; !platform.isSupported() && (i < Gamepad.PlatformFactories.length); i++) { platform = Gamepad.PlatformFactories[i](listener); } return platform; }; /** * Registers given gamepad. * * @method _connect * @param {Object} gamepad Gamepad to connect to * @private */ Gamepad.prototype._connect = function(gamepad) { var mapping = this._resolveMapping(gamepad); var count; var i; //gamepad.mapping = this._resolveMapping(gamepad); gamepad.state = {}; gamepad.lastState = {}; gamepad.updater = []; count = mapping.buttons.byButton.length; for (i = 0; i < count; i++) { this._addButtonUpdater(gamepad, mapping, i); } count = mapping.axes.byAxis.length; for (i = 0; i < count; i++) { this._addAxisUpdater(gamepad, mapping, i); } this.gamepads[gamepad.index] = gamepad; this._fire(Gamepad.Event.CONNECTED, gamepad); }; /** * Adds an updater for a button control * * @method _addButtonUpdater * @private * @param {Object} gamepad the gamepad for which to create the updater * @param {Object} mapping the mapping on which to work on * @param {Number} index button index */ Gamepad.prototype._addButtonUpdater = function(gamepad, mapping, index) { var updater = nullFunction; var controlName = getControlName(Gamepad.StandardButtons, index, 'EXTRA_BUTTON_'); var getter = this._createButtonGetter(gamepad, mapping.buttons, index); var that = this; var buttonEventData = { gamepad: gamepad, control: controlName }; gamepad.state[controlName] = 0; gamepad.lastState[controlName] = 0; updater = function() { var value = getter(); var lastValue = gamepad.lastState[controlName]; var isDown = value > 0.5; var wasDown = lastValue > 0.5; gamepad.state[controlName] = value; if (isDown && !wasDown) { that._fire(Gamepad.Event.BUTTON_DOWN, Object.create(buttonEventData)); } else if (!isDown && wasDown) { that._fire(Gamepad.Event.BUTTON_UP, Object.create(buttonEventData)); } if ((value !== 0) && (value !== 1) && (value !== lastValue)) { that._fireAxisChangedEvent(gamepad, controlName, value); } gamepad.lastState[controlName] = value; }; gamepad.updater.push(updater); }; /** * Adds an updater for an axis control * * @method _addAxisUpdater * @private * @param {Object} gamepad the gamepad for which to create the updater * @param {Object} mapping the mapping on which to work on * @param {Number} index axis index */ Gamepad.prototype._addAxisUpdater = function(gamepad, mapping, index) { var updater = nullFunction; var controlName = getControlName(Gamepad.StandardAxes, index, 'EXTRA_AXIS_'); var getter = this._createAxisGetter(gamepad, mapping.axes, index); var that = this; gamepad.state[controlName] = 0; gamepad.lastState[controlName] = 0; updater = function() { var value = getter(); var lastValue = gamepad.lastState[controlName]; gamepad.state[controlName] = value; if ((value !== lastValue)) { that._fireAxisChangedEvent(gamepad, controlName, value); } gamepad.lastState[controlName] = value; }; gamepad.updater.push(updater); }; /** * Fires an AXIS_CHANGED event * @method _fireAxisChangedEvent * @private * @param {Object} gamepad the gamepad to notify for * @param {String} controlName name of the control that changes its value * @param {Number} value the new value */ Gamepad.prototype._fireAxisChangedEvent = function(gamepad, controlName, value) { var eventData = { gamepad: gamepad, axis: controlName, value: value }; this._fire(Gamepad.Event.AXIS_CHANGED, eventData); }; /** * Creates a getter according to the mapping entry for the specific index. * Currently supported entries: * * buttons.byButton[index]: Number := Index into gamepad.buttons; -1 tests byAxis * buttons.byAxis[index]: Array := [Index into gamepad.axes; Zero Value, One Value] * * @method _createButtonGetter * @private * @param {Object} gamepad the gamepad for which to create a getter * @param {Object} buttons the mappings entry for the buttons * @param {Number} index the specific button entry * @return {Function} a getter returning the value for the requested button */ Gamepad.prototype._createButtonGetter = (function() { var nullGetter = function() { return 0; }; var createRangeGetter = function(valueGetter, from, to) { var getter = nullGetter; if (from < to) { getter = function() { var range = to - from; var value = valueGetter(); value = (value - from) / range; return (value < 0) ? 0 : value; }; } else if (to < from) { getter = function() { var range = from - to; var value = valueGetter(); value = (value - to) / range; return (value > 1) ? 0 : (1 - value); }; } return getter; }; var isArray = function(thing) { return Object.prototype.toString.call(thing) === '[object Array]'; }; return function(gamepad, buttons, index) { var getter = nullGetter; var entry; var that = this; entry = buttons.byButton[index]; if (entry !== -1) { if ((typeof(entry) === 'number') && (entry < gamepad.buttons.length)) { getter = function() { return gamepad.buttons[entry]; }; } } else if (buttons.byAxis && (index < buttons.byAxis.length)) { entry = buttons.byAxis[index]; if (isArray(entry) && (entry.length == 3) && (entry[0] < gamepad.axes.length)) { getter = function() { var value = gamepad.axes[entry[0]]; return that._applyDeadzoneMaximize(value); }; getter = createRangeGetter(getter, entry[1], entry[2]); } } return getter; }; })(); /** * Creates a getter according to the mapping entry for the specific index. * Currently supported entries: * * axes.byAxis[index]: Number := Index into gamepad.axes; -1 ignored * * @method _createAxisGetter * @private * @param {Object} gamepad the gamepad for which to create a getter * @param {Object} axes the mappings entry for the axes * @param {Number} index the specific axis entry * @return {Function} a getter returning the value for the requested axis */ Gamepad.prototype._createAxisGetter = (function() { var nullGetter = function() { return 0; }; return function(gamepad, axes, index) { var getter = nullGetter; var entry; var that = this; entry = axes.byAxis[index]; if (entry !== -1) { if ((typeof(entry) === 'number') && (entry < gamepad.axes.length)) { getter = function() { var value = gamepad.axes[entry]; return that._applyDeadzoneMaximize(value); }; } } return getter; }; })(); /** * Disconnects from given gamepad. * * @method _disconnect * @param {Object} gamepad Gamepad to disconnect * @private */ Gamepad.prototype._disconnect = function(gamepad) { var newGamepads = [], i; if (typeof(this.gamepads[gamepad.index]) !== 'undefined') { delete this.gamepads[gamepad.index]; } for (i = 0; i < this.gamepads.length; i++) { if (typeof(this.gamepads[i]) !== 'undefined') { newGamepads[i] = this.gamepads[i]; } } this.gamepads = newGamepads; this._fire(Gamepad.Event.DISCONNECTED, gamepad); }; /** * Resolves controller type from its id. * * @method _resolveControllerType * @param {String} id Controller id * @return {String} Controller type, one of Gamepad.Type * @private */ Gamepad.prototype._resolveControllerType = function(id) { id = id.toLowerCase(); if (id.indexOf('playstation') !== -1) { return Gamepad.Type.PLAYSTATION; } else if ( id.indexOf('logitech') !== -1 || id.indexOf('wireless gamepad') !== -1) { return Gamepad.Type.LOGITECH; } else if (id.indexOf('xbox') !== -1 || id.indexOf('360') !== -1) { return Gamepad.Type.XBOX; } else { return Gamepad.Type.UNKNOWN; } }; /** * @method _resolveMapping * @private * @param {Object} gamepad the gamepad for which to resolve the mapping * @return {Object} a mapping object for the given gamepad */ Gamepad.prototype._resolveMapping = function(gamepad) { var mappings = Gamepad.Mappings; var mapping = null; var env = { platform: this.platform.getType(), type: this._resolveControllerType(gamepad.id) }; var i; var test; for (i = 0; !mapping && (i < mappings.length); i++) { test = mappings[i]; if (Gamepad.envMatchesFilter(test.env, env)) { mapping = test; } } return mapping || Gamepad.StandardMapping; }; /** * @method envMatchesFilter * @static * @param {Object} filter the filter object describing properties to match * @param {Object} env the environment object that is matched against filter * @return {Boolean} true if env is covered by filter */ Gamepad.envMatchesFilter = function(filter, env) { var result = true; var field; for (field in filter) { if (filter[field] !== env[field]) { result = false; } } return result; }; /** * Updates the controllers, triggering TICK events. * * @method _update * @private */ Gamepad.prototype._update = function() { this.platform.update(); this.gamepads.forEach(function(gamepad) { if (gamepad) { gamepad.updater.forEach(function(updater) { updater(); }); } }); if (this.gamepads.length > 0) { this._fire(Gamepad.Event.TICK, this.gamepads); } }, /** * Applies deadzone and maximization. * * You can change the thresholds via deadzone and maximizeThreshold members. * * @method _applyDeadzoneMaximize * @param {Number} value Value to modify * @param {Number} [deadzone] Deadzone to apply * @param {Number} [maximizeThreshold] From which value to maximize value * @private */ Gamepad.prototype._applyDeadzoneMaximize = function( value, deadzone, maximizeThreshold) { deadzone = typeof(deadzone) !== 'undefined' ? deadzone : this.deadzone; maximizeThreshold = typeof(maximizeThreshold) !== 'undefined' ? maximizeThreshold : this.maximizeThreshold; if (value >= 0) { if (value < deadzone) { value = 0; } else if (value > maximizeThreshold) { value = 1; } } else { if (value > -deadzone) { value = 0; } else if (value < -maximizeThreshold) { value = -1; } } return value; }; exports.Gamepad = Gamepad; })(((typeof(module) !== 'undefined') && module.exports) || window);