UNPKG

shaku

Version:

A simple and effective JavaScript game development framework that knows its place!

1,318 lines (1,175 loc) 49.7 kB
/** * Implement the input manager. * * |-- copyright and license --| * @module Shaku * @file shaku\src\input\input.js * @author Ronen Ness (ronenness@gmail.com | http://ronenness.com) * @copyright (c) 2021 Ronen Ness * @license MIT * |-- end copyright and license --| * */ 'use strict'; const IManager = require('../manager.js'); const Vector2 = require('../utils/vector2.js'); const { MouseButton, MouseButtons, KeyboardKey, KeyboardKeys } = require('./key_codes.js'); const Gamepad = require('./gamepad'); const _logger = require('../logger.js').getLogger('input'); // get timestamp function timestamp() { return (new Date()).getTime(); } // touch key code const _touchKeyCode = "touch"; /** * Input manager. * Used to recieve input from keyboard and mouse. * * To access the Input manager use `Shaku.input`. */ class Input extends IManager { /** * Create the manager. */ constructor() { super(); // callbacks and target we listen to input on this._callbacks = null; this._targetElement = window; /** * If true, will prevent default input events by calling preventDefault(). * @name Input#preventDefaults * @type {Boolean} */ this.preventDefaults = false; /** * By default, when holding wheel button down browsers will turn into special page scroll mode and will not emit mouse move events. * if this property is set to true (default), the Input manager will prevent this behavior, so we could still get mouse delta while mouse wheel is held down. * @name Input#disableMouseWheelAutomaticScrolling * @type {Boolean} */ this.disableMouseWheelAutomaticScrolling = true; /** * If true (default), will disable the context menu (what typically opens when you right click the page). * @name Input#disableContextMenu * @type {Boolean} */ this.disableContextMenu = true; /** * If true (default), will treat touch events (touch start / touch end / touch move) as if the user clicked and moved a mouse. * @name Input#delegateTouchInputToMouse * @type {Boolean} */ this.delegateTouchInputToMouse = true; /** * If true (default), will delegate events from mapped gamepads to custom keys. * This will add the following codes to all basic query methods (down, pressed, released, doublePressed, doubleReleased): * - gamepadX_top: state of arrow keys top key (left buttons). * - gamepadX_bottom: state of arrow keys bottom key (left buttons). * - gamepadX_left: state of arrow keys left key (left buttons). * - gamepadX_right: state of arrow keys right key (left buttons). * - gamepadX_leftStickUp: true if left stick points directly up. * - gamepadX_leftStickDown: true if left stick points directly down. * - gamepadX_leftStickLeft: true if left stick points directly left. * - gamepadX_leftStickRight: true if left stick points directly right. * - gamepadX_rightStickUp: true if right stick points directly up. * - gamepadX_rightStickDown: true if right stick points directly down. * - gamepadX_rightStickLeft: true if right stick points directly left. * - gamepadX_rightStickRight: true if right stick points directly right. * - gamepadX_a: state of A key (from right buttons). * - gamepadX_b: state of B key (from right buttons). * - gamepadX_x: state of X key (from right buttons). * - gamepadX_y: state of Y key (from right buttons). * - gamepadX_frontTopLeft: state of the front top-left button. * - gamepadX_frontTopRight: state of the front top-right button. * - gamepadX_frontBottomLeft: state of the front bottom-left button. * - gamepadX_frontBottomRight: state of the front bottom-right button. * Where X in `gamepad` is the gamepad index: gamepad0, gamepad1, gamepad2.. * @name Input#delegateGamepadInputToKeys * @type {Boolean} */ this.delegateGamepadInputToKeys = true; /** * If true (default), will reset all states if the window loses focus. * @name Input#resetOnFocusLoss * @type {Boolean} */ this.resetOnFocusLoss = true; /** * Default time, in milliseconds, to consider two consecutive key presses as a double-press. * @name Input#defaultDoublePressInterval * @type {Number} */ this.defaultDoublePressInterval = 250; // set base state members this.#_resetAll(); } /** * Get the Mouse Buttons enum. * @see MouseButtons */ get MouseButtons() { return MouseButtons; } /** * Get the Keyboard Buttons enum. * @see KeyboardKeys */ get KeyboardKeys() { return KeyboardKeys; } /** * Return the string code to use in order to get touch events. * @returns {String} Key code to use for touch events. */ get TouchKeyCode() { return _touchKeyCode; } /** * @inheritdoc * @private **/ setup() { return new Promise((resolve, reject) => { _logger.info("Setup input manager.."); // if target element is a method, invoke it if (typeof this._targetElement === 'function') { this._targetElement = this._targetElement(); if (!this._targetElement) { throw new Error("Input target element was set to be a method, but the returned value was invalid!"); } } // get element to attach to let element = this._targetElement; // to make sure keyboard input would work if provided with canvas entity if (element.tabIndex === -1 || element.tabIndex === undefined) { element.tabIndex = 1000; } // focus on target element window.setTimeout(() => element.focus(), 0); // set all the events to listen to var _this = this; this._callbacks = { 'mousedown': function(event) {_this._onMouseDown(event); if (this.preventDefaults) event.preventDefault(); }, 'mouseup': function(event) {_this._onMouseUp(event); if (this.preventDefaults) event.preventDefault(); }, 'mousemove': function(event) {_this._onMouseMove(event); if (this.preventDefaults) event.preventDefault(); }, 'keydown': function(event) {_this._onKeyDown(event); if (this.preventDefaults) event.preventDefault(); }, 'keyup': function(event) {_this._onKeyUp(event); if (this.preventDefaults) event.preventDefault(); }, 'blur': function(event) {_this._onBlur(event); if (this.preventDefaults) event.preventDefault(); }, 'wheel': function(event) {_this._onMouseWheel(event); if (this.preventDefaults) event.preventDefault(); }, 'touchstart': function(event) {_this._onTouchStart(event); if (this.preventDefaults) event.preventDefault(); }, 'touchend': function(event) {_this._onTouchEnd(event); if (this.preventDefaults) event.preventDefault(); }, 'touchmove': function(event) {_this._onTouchMove(event); if (this.preventDefaults) event.preventDefault(); }, 'contextmenu': function(event) { if (_this.disableContextMenu) { event.preventDefault(); } }, }; // reset all data to init initial state this.#_resetAll(); // register all callbacks for (var event in this._callbacks) { element.addEventListener(event, this._callbacks[event], false); } // if we have a specific element, still capture mouse release outside of it if (element !== window) { window.addEventListener('mouseup', this._callbacks['mouseup'], false); window.addEventListener('touchend', this._callbacks['touchend'], false); } // custom keys set this._customKeys = new Set(); // ready! resolve(); }); } /** * @inheritdoc * @private **/ startFrame() { // query gamepads const prevGamepadData = this._gamepadsData || []; const prevDefaultGamepadId = (this._defaultGamepad || {id: 'null'}).id; this._gamepadsData = navigator.getGamepads(); // get default gamepad and check for changes this._defaultGamepad = null; let i = 0; for (let gp of this._gamepadsData) { let newId = (gp || {id: 'null'}).id; let prevId = (prevGamepadData[i] || {id: 'null'}).id; if (newId !== prevId) { if (newId !== 'null') { _logger.info(`Gamepad ${i} connected: ${newId}.`); } else if (newId === 'null') { _logger.info(`Gamepad ${i} disconnected: ${prevId}.`); } } if (gp && !this._defaultGamepad) { this._defaultGamepad = gp; this._defaultGamepadIndex = i; } i++; } // changed default gamepad? const newDefaultGamepadId = (this._defaultGamepad || {id: 'null'}).id; if (newDefaultGamepadId !== prevDefaultGamepadId) { _logger.info(`Default gamepad changed from '${prevDefaultGamepadId}' to '${newDefaultGamepadId}'.`); } // reset queried gamepad states this._queriedGamepadStates = {}; // delegate gamepad keys if (this.delegateGamepadInputToKeys) { for (let i = 0; i < 4; ++i) { // get current gamepad const gp = this.gamepad(i); // not set or not mapped? reset all values to false and continue if (!gp || !gp.isMapped) { this.setCustomState(`gamepad${i}_top`, false); this.setCustomState(`gamepad${i}_bottom`, false); this.setCustomState(`gamepad${i}_left`, false); this.setCustomState(`gamepad${i}_right`, false); this.setCustomState(`gamepad${i}_y`, false); this.setCustomState(`gamepad${i}_a`, false); this.setCustomState(`gamepad${i}_x`, false); this.setCustomState(`gamepad${i}_b`, false); this.setCustomState(`gamepad${i}_frontTopLeft`, false); this.setCustomState(`gamepad${i}_frontTopRight`, false); this.setCustomState(`gamepad${i}_frontBottomLeft`, false); this.setCustomState(`gamepad${i}_frontBottomRight`, false); this.setCustomState(`gamepad${i}_leftStickUp`, false); this.setCustomState(`gamepad${i}_leftStickDown`, false); this.setCustomState(`gamepad${i}_leftStickLeft`, false); this.setCustomState(`gamepad${i}_leftStickRight`, false); this.setCustomState(`gamepad${i}_rightStickUp`, false); this.setCustomState(`gamepad${i}_rightStickDown`, false); this.setCustomState(`gamepad${i}_rightStickLeft`, false); this.setCustomState(`gamepad${i}_rightStickRight`, false); continue; } // set actual values this.setCustomState(`gamepad${i}_top`, gp.leftButtons.top); this.setCustomState(`gamepad${i}_bottom`, gp.leftButtons.bottom); this.setCustomState(`gamepad${i}_left`, gp.leftButtons.left); this.setCustomState(`gamepad${i}_right`, gp.leftButtons.right); this.setCustomState(`gamepad${i}_y`, gp.rightButtons.top); this.setCustomState(`gamepad${i}_a`, gp.rightButtons.bottom); this.setCustomState(`gamepad${i}_x`, gp.rightButtons.left); this.setCustomState(`gamepad${i}_b`, gp.rightButtons.right); this.setCustomState(`gamepad${i}_frontTopLeft`, gp.frontButtons.topLeft); this.setCustomState(`gamepad${i}_frontTopRight`, gp.frontButtons.topRight); this.setCustomState(`gamepad${i}_frontBottomLeft`, gp.frontButtons.bottomLeft); this.setCustomState(`gamepad${i}_frontBottomRight`, gp.frontButtons.bottomRight); this.setCustomState(`gamepad${i}_leftStickUp`, gp.leftStick.y < -0.8); this.setCustomState(`gamepad${i}_leftStickDown`, gp.leftStick.y > 0.8); this.setCustomState(`gamepad${i}_leftStickLeft`, gp.leftStick.x < -0.8); this.setCustomState(`gamepad${i}_leftStickRight`, gp.leftStick.x > 0.8); this.setCustomState(`gamepad${i}_rightStickUp`, gp.rightStick.y < -0.8); this.setCustomState(`gamepad${i}_rightStickDown`, gp.rightStick.y > 0.8); this.setCustomState(`gamepad${i}_rightStickLeft`, gp.rightStick.x < -0.8); this.setCustomState(`gamepad${i}_rightStickRight`, gp.rightStick.x > 0.8); } } } /** * @inheritdoc * @private **/ destroy() { // unregister all callbacks if (this._callbacks) { let element = this._targetElement; for (var event in this._callbacks) { element.removeEventListener(event, this._callbacks[event]); } if (element !== window) { window.removeEventListener('mouseup', this._callbacks['mouseup'], false); window.removeEventListener('touchend', this._callbacks['touchend'], false); } this._callbacks = null; } } /** * Set the target element to attach input to. If not called, will just use the entire document. * Must be called *before* initializing Shaku. This can also be a method to invoke while initializing. * @example * // the following will use whatever canvas the gfx manager uses as input element. * // this means mouse offset will also be relative to this element. * Shaku.input.setTargetElement(() => Shaku.gfx.canvas); * @param {Element | elementCallback} element Element to attach input to. */ setTargetElement(element) { if (this._callbacks) { throw new Error("'setTargetElement() must be called before initializing the input manager!"); } this._targetElement = element; } /** * Reset all internal data and states. * @param {Boolean=} keepMousePosition If true, will not reset mouse position. * @private */ #_resetAll(keepMousePosition) { // mouse states if (!keepMousePosition) { this._mousePos = new Vector2(); this._mousePrevPos = new Vector2(); } this._mouseState = {}; this._mouseWheel = 0; // touching states if (!keepMousePosition) { this._touchPosition = new Vector2(); } this._isTouching = false; this._touchStarted = false; this._touchEnded = false; // keyboard keys this._keyboardState = {}; this._keyboardPrevState = {}; // reset pressed / release events this._keyboardPressed = {}; this._keyboardReleased = {}; this._mousePressed = {}; this._mouseReleased = {}; // for custom states this._customStates = {}; this._customPressed = {}; this._customReleased = {}; this._lastCustomReleasedTime = {}; this._lastCustomPressedTime = {}; this._prevLastCustomReleasedTime = {}; this._prevLastCustomPressedTime = {}; // for last release time and to count double click / double pressed events this._lastMouseReleasedTime = {}; this._lastKeyReleasedTime = {}; this._lastTouchReleasedTime = 0; this._lastMousePressedTime = {}; this._lastKeyPressedTime = {}; this._lastTouchPressedTime = 0; this._prevLastMouseReleasedTime = {}; this._prevLastKeyReleasedTime = {}; this._prevLastTouchReleasedTime = 0; this._prevLastMousePressedTime = {}; this._prevLastKeyPressedTime = {}; this._prevLastTouchPressedTime = 0; // currently connected gamepads data this._defaultGamepad = null; this._gamepadsData = []; this._queriedGamepadStates = {}; } /** * Get Gamepad current states, or null if not connected. * Note: this object does not update itself, you'll need to query it again every frame. * @param {Number=} index Gamepad index or undefined for first connected device. * @returns {Gamepad} Gamepad current state. */ gamepad(index) { // default index if (index === null || index === undefined) { index = this._defaultGamepadIndex; } // try to get cached value let cached = this._queriedGamepadStates[index]; // not found? create if (!cached) { const gp = this._gamepadsData[index]; if (!gp) { return null; } this._queriedGamepadStates[index] = cached = new Gamepad(gp); } // return gamepad state return cached; } /** * Get gamepad id, or null if not connected to this slot. * @param {Number=} index Gamepad index or undefined for first connected device. * @returns Gamepad id or null. */ gamepadId(index) { return this.gamepadIds()[index || 0] || null; } /** * Return a list with connected devices ids. * @returns {Array<String>} List of connected devices ids. */ gamepadIds() { let ret = []; for (let gp of this._gamepadsData) { if (gp) { ret.push(gp.id); } } return ret; } /** * Get touch screen touching position. * Note: if not currently touching, will return last known position. * @returns {Vector2} Touch position. */ get touchPosition() { return this._touchPosition.clone(); } /** * Get if currently touching a touch screen. * @returns {Boolean} True if currently touching the screen. */ get touching() { return this._isTouching; } /** * Get if started touching a touch screen in current frame. * @returns {Boolean} True if started touching the screen now. */ get touchStarted() { return this._touchStarted; } /** * Get if stopped touching a touch screen in current frame. * @returns {Boolean} True if stopped touching the screen now. */ get touchEnded() { return this._touchEnded; } /** * Set a custom key code state you can later use with all the built in methods (down / pressed / released / doublePressed, etc.) * For example, lets say you want to implement a simulated keyboard and use it alongside the real keyboard. * When your simulated keyboard space key is pressed, you can call `setCustomState('sim_space', true)`. When released, call `setCustomState('sim_space', false)`. * Now you can use `Shaku.input.down(['space', 'sim_space'])` to check if either a real space or simulated space is pressed down. * @param {String} code Code to set state for. * @param {Boolean|null} value Current value to set, or null to remove custom key. */ setCustomState(code, value) { // remove custom value if (value === null) { this._customKeys.delete(code); delete this._customPressed[code]; delete this._customReleased[code]; delete this._customStates[code]; return; } // update custom codes else { this._customKeys.add(code); } // set state value = Boolean(value); const prev = Boolean(this._customStates[code]); this._customStates[code] = value; // set defaults if (this._customPressed[code] === undefined) { this._customPressed[code] = false; } if (this._customReleased[code] === undefined) { this._customReleased[code] = false; } // pressed now? if (!prev && value) { this._customPressed[code] = true; this._prevLastCustomPressedTime[code] = this._lastCustomPressedTime[code]; this._lastCustomPressedTime[code] = timestamp(); } // released now? if (prev && !value) { this._customReleased[code] = true; this._prevLastCustomReleasedTime[code] = this._lastCustomReleasedTime[code]; this._lastCustomReleasedTime[code] = timestamp(); } } /** * Get mouse position. * @returns {Vector2} Mouse position. */ get mousePosition() { return this._mousePos.clone(); } /** * Get mouse previous position (before the last endFrame() call). * @returns {Vector2} Mouse position in previous frame. */ get prevMousePosition() { return (this._mousePrevPos || this._mousePos).clone(); } /** * Get mouse movement since last endFrame() call. * @returns {Vector2} Mouse change since last frame. */ get mouseDelta() { // no previous position? return 0,0. if (!this._mousePrevPos) { return Vector2.zero(); } // return mouse delta return new Vector2(this._mousePos.x - this._mousePrevPos.x, this._mousePos.y - this._mousePrevPos.y); } /** * Get if mouse is currently moving. * @returns {Boolean} True if mouse moved since last frame, false otherwise. */ get mouseMoving() { return (this._mousePrevPos && !this._mousePrevPos.equals(this._mousePos)); } /** * Get if mouse button was pressed this frame. * @param {MouseButton} button Button code (defults to MouseButtons.left). * @returns {Boolean} True if mouse button is currently down, but was up in previous frame. */ mousePressed(button = 0) { if (button === undefined) throw new Error("Invalid button code!"); return Boolean(this._mousePressed[button]); } /** * Get if mouse button is currently pressed. * @param {MouseButton} button Button code (defults to MouseButtons.left). * @returns {Boolean} true if mouse button is currently down, false otherwise. */ mouseDown(button = 0) { if (button === undefined) throw new Error("Invalid button code!"); return Boolean(this._mouseState[button]); } /** * Get if mouse button is currently not down. * @param {MouseButton} button Button code (defults to MouseButtons.left). * @returns {Boolean} true if mouse button is currently up, false otherwise. */ mouseUp(button = 0) { if (button === undefined) throw new Error("Invalid button code!"); return Boolean(!this.mouseDown(button)); } /** * Get if mouse button was released in current frame. * @param {MouseButton} button Button code (defults to MouseButtons.left). * @returns {Boolean} True if mouse was down last frame, but released in current frame. */ mouseReleased(button = 0) { if (button === undefined) throw new Error("Invalid button code!"); return Boolean(this._mouseReleased[button]); } /** * Get if keyboard key is currently pressed down. * @param {KeyboardKey} key Keyboard key code. * @returns {boolean} True if keyboard key is currently down, false otherwise. */ keyDown(key) { if (key === undefined) throw new Error("Invalid key code!"); return Boolean(this._keyboardState[key]); } /** * Get if keyboard key is currently not down. * @param {KeyboardKey} key Keyboard key code. * @returns {Boolean} True if keyboard key is currently up, false otherwise. */ keyUp(key) { if (key === undefined) throw new Error("Invalid key code!"); return Boolean(!this.keyDown(key)); } /** * Get if a keyboard button was released in current frame. * @param {KeyboardKey} button Keyboard key code. * @returns {Boolean} True if key was down last frame, but released in current frame. */ keyReleased(key) { if (key === undefined) throw new Error("Invalid key code!"); return Boolean(this._keyboardReleased[key]); } /** * Get if keyboard key was pressed this frame. * @param {KeyboardKey} key Keyboard key code. * @returns {Boolean} True if key is currently down, but was up in previous frame. */ keyPressed(key) { if (key === undefined) throw new Error("Invalid key code!"); return Boolean(this._keyboardPressed[key]); } /** * Get if any of the shift keys are currently down. * @returns {Boolean} True if there's a shift key pressed down. */ get shiftDown() { return Boolean(this.keyDown(this.KeyboardKeys.shift)); } /** * Get if any of the Ctrl keys are currently down. * @returns {Boolean} True if there's a Ctrl key pressed down. */ get ctrlDown() { return Boolean(this.keyDown(this.KeyboardKeys.ctrl)); } /** * Get if any of the Alt keys are currently down. * @returns {Boolean} True if there's an Alt key pressed down. */ get altDown() { return Boolean(this.keyDown(this.KeyboardKeys.alt)); } /** * Get if any keyboard key was pressed this frame. * @returns {Boolean} True if any key was pressed down this frame. */ get anyKeyPressed() { return Object.keys(this._keyboardPressed).length !== 0; } /** * Get if any keyboard key is currently down. * @returns {Boolean} True if there's a key pressed down. */ get anyKeyDown() { for (var key in this._keyboardState) { if (this._keyboardState[key]) { return true; } } return false; } /** * Get if any mouse button was pressed this frame. * @returns {Boolean} True if any of the mouse buttons were pressed this frame. */ get anyMouseButtonPressed() { return Object.keys(this._mousePressed).length !== 0; } /** * Get if any mouse button is down. * @returns {Boolean} True if any of the mouse buttons are pressed. */ get anyMouseButtonDown() { for (var key in this._mouseState) { if (this._mouseState[key]) { return true; } } return false; } /** * Return if a mouse or keyboard state in a generic way. Used internally. * @private * @param {string} code Keyboard, mouse or touch code. * For mouse buttons: mouse_left, mouse_right or mouse_middle. * For keyboard buttons: use one of the keys of KeyboardKeys (for example 'a', 'alt', 'up_arrow', etc..). * For touch: just use 'touch' as code. * For numbers (0-9): you can use the number. * @param {Function} mouseCheck Callback to use to return value if its a mouse button code. * @param {Function} keyboardCheck Callback to use to return value if its a keyboard key code. * @param {*} touchValue Value to use to return value if its a touch code. * @param {*} customValues Dictionary to check for custom values injected via setCustomState(). */ #_getValueWithCode(code, mouseCheck, keyboardCheck, touchValue, customValues) { // make sure code is string code = String(code); // check for custom values const customVal = customValues[code]; if (customVal !== undefined) { return customVal; } if (this._customKeys.has(code)) { return false; } // if its 'touch' its for touch events if (code === _touchKeyCode) { return touchValue; } // if starts with 'mouse' its for mouse button events if (code.indexOf('mouse_') === 0) { // get mouse code name const codename = code.split('_')[1]; const mouseKey = this.MouseButtons[codename]; if (mouseKey === undefined) { throw new Error("Unknown mouse button: " + code); } // return if mouse down return mouseCheck.call(this, mouseKey); } // if its just a number, add the 'n' prefix if (!isNaN(parseInt(code)) && code.length === 1) { code = 'n' + code; } // if not start with 'mouse', treat it as a keyboard key const keyboardKey = this.KeyboardKeys[code]; if (keyboardKey === undefined) { throw new Error("Unknown keyboard key: " + code); } return keyboardCheck.call(this, this.KeyboardKeys[code]); } /** * Return if a mouse or keyboard button is currently down. * @example * if (Shaku.input.down(['mouse_left', 'touch', 'space'])) { alert('mouse, touch screen or space are pressed!'); } * @param {string|Array<String>} code Keyboard, touch or mouse code. Can be array of codes to test any of them. * For mouse buttons: set code to 'mouse_left', 'mouse_right' or 'mouse_middle'. * For keyboard buttons: use one of the keys of KeyboardKeys (for example 'a', 'alt', 'up_arrow', etc..). * For touch screen: set code to 'touch'. * For numbers (0-9): you can use the number itself. * Note: if you inject any custom state via `setCustomState()`, you can use its code here too. * @returns {Boolean} True if key or mouse button are down. */ down(code) { if (!Array.isArray(code)) { code = [code]; } for (let c of code) { if (Boolean(this.#_getValueWithCode(c, this.mouseDown, this.keyDown, this.touching, this._customStates))) { return true; } } return false; } /** * Return if a mouse or keyboard button was released in this frame. * @example * if (Shaku.input.released(['mouse_left', 'touch', 'space'])) { alert('mouse, touch screen or space were released!'); } * @param {string|Array<String>} code Keyboard, touch, gamepad or mouse button code. Can be array of codes to test any of them. * For mouse buttons: set code to 'mouse_left', 'mouse_right' or 'mouse_middle'. * For keyboard buttons: use one of the keys of KeyboardKeys (for example 'a', 'alt', 'up_arrow', etc..). * For touch screen: set code to 'touch'. * For numbers (0-9): you can use the number itself. * Note: if you inject any custom state via `setCustomState()`, you can use its code here too. * @returns {Boolean} True if key or mouse button were down in previous frame, and released this frame. */ released(code) { if (!Array.isArray(code)) { code = [code]; } for (let c of code) { if (Boolean(this.#_getValueWithCode(c, this.mouseReleased, this.keyReleased, this.touchEnded, this._customReleased))) { return true; } } return false; } /** * Return if a mouse or keyboard button was pressed in this frame. * @example * if (Shaku.input.pressed(['mouse_left', 'touch', 'space'])) { alert('mouse, touch screen or space were pressed!'); } * @param {string|Array<String>} code Keyboard, touch, gamepad or mouse button code. Can be array of codes to test any of them. * For mouse buttons: set code to 'mouse_left', 'mouse_right' or 'mouse_middle'. * For keyboard buttons: use one of the keys of KeyboardKeys (for example 'a', 'alt', 'up_arrow', etc..). * For touch screen: set code to 'touch'. * For numbers (0-9): you can use the number itself. * Note: if you inject any custom state via `setCustomState()`, you can use its code here too. * @returns {Boolean} True if key or mouse button where up in previous frame, and pressed this frame. */ pressed(code) { if (!Array.isArray(code)) { code = [code]; } for (let c of code) { if (Boolean(this.#_getValueWithCode(c, this.mousePressed, this.keyPressed, this.touchStarted, this._customPressed))) { return true; } } return false; } /** * Return timestamp, in milliseconds, of the last time this key code was released. * @example * let lastReleaseTime = Shaku.input.lastReleaseTime('mouse_left'); * @param {string} code Keyboard, touch, gamepad or mouse button code. * For mouse buttons: set code to 'mouse_left', 'mouse_right' or 'mouse_middle'. * For keyboard buttons: use one of the keys of KeyboardKeys (for example 'a', 'alt', 'up_arrow', etc..). * For touch screen: set code to 'touch'. * For numbers (0-9): you can use the number itself. * Note: if you inject any custom state via `setCustomState()`, you can use its code here too. * @returns {Number} Timestamp of last key release, or 0 if was never released. */ lastReleaseTime(code) { if (Array.isArray(code)) { throw new Error("Array not supported in 'lastReleaseTime'!"); } return this.#_getValueWithCode(code, (c) => this._lastMouseReleasedTime[c], (c) => this._lastKeyReleasedTime[c], this._lastTouchReleasedTime, this._prevLastCustomReleasedTime) || 0; } /** * Return timestamp, in milliseconds, of the last time this key code was pressed. * @example * let lastPressTime = Shaku.input.lastPressTime('mouse_left'); * @param {string} code Keyboard, touch, gamepad or mouse button code. * For mouse buttons: set code to 'mouse_left', 'mouse_right' or 'mouse_middle'. * For keyboard buttons: use one of the keys of KeyboardKeys (for example 'a', 'alt', 'up_arrow', etc..). * For touch screen: set code to 'touch'. * For numbers (0-9): you can use the number itself. * Note: if you inject any custom state via `setCustomState()`, you can use its code here too. * @returns {Number} Timestamp of last key press, or 0 if was never pressed. */ lastPressTime(code) { if (Array.isArray(code)) { throw new Error("Array not supported in 'lastPressTime'!"); } return this.#_getValueWithCode(code, (c) => this._lastMousePressedTime[c], (c) => this._lastKeyPressedTime[c], this._lastTouchPressedTime, this._prevLastCustomPressedTime) || 0; } /** * Return if a key was double-pressed. * @example * let doublePressed = Shaku.input.doublePressed(['mouse_left', 'touch', 'space']); * @param {string|Array<string>} code Keyboard, touch, gamepad or mouse button code. Can be array of codes to test any of them. * For mouse buttons: set code to 'mouse_left', 'mouse_right' or 'mouse_middle'. * For keyboard buttons: use one of the keys of KeyboardKeys (for example 'a', 'alt', 'up_arrow', etc..). * For touch screen: set code to 'touch'. * For numbers (0-9): you can use the number itself. * Note: if you inject any custom state via `setCustomState()`, you can use its code here too. * @param {Number} maxInterval Max interval time, in milliseconds, to consider it a double-press. Defaults to `defaultDoublePressInterval`. * @returns {Boolean} True if one or more key codes double-pressed, false otherwise. */ doublePressed(code, maxInterval) { // default interval maxInterval = maxInterval || this.defaultDoublePressInterval; // current timestamp let currTime = timestamp(); // check all keys if (!Array.isArray(code)) { code = [code]; } for (let c of code) { if (this.pressed(c)) { let currKeyTime = this.#_getValueWithCode(c, (c) => this._prevLastMousePressedTime[c], (c) => this._prevLastKeyPressedTime[c], this._prevLastTouchPressedTime, this._prevLastCustomPressedTime); if (currTime - currKeyTime <= maxInterval) { return true; } } } return false; } /** * Return if a key was double-released. * @example * let doubleReleased = Shaku.input.doubleReleased(['mouse_left', 'touch', 'space']); * @param {string|Array<string>} code Keyboard, touch, gamepad or mouse button code. Can be array of codes to test any of them. * For mouse buttons: set code to 'mouse_left', 'mouse_right' or 'mouse_middle'. * For keyboard buttons: use one of the keys of KeyboardKeys (for example 'a', 'alt', 'up_arrow', etc..). * For touch screen: set code to 'touch'. * For numbers (0-9): you can use the number itself. * Note: if you inject any custom state via `setCustomState()`, you can use its code here too. * @param {Number} maxInterval Max interval time, in milliseconds, to consider it a double-release. Defaults to `defaultDoublePressInterval`. * @returns {Boolean} True if one or more key codes double-released, false otherwise. */ doubleReleased(code, maxInterval) { // default interval maxInterval = maxInterval || this.defaultDoublePressInterval; // current timestamp let currTime = timestamp(); // check all keys if (!Array.isArray(code)) { code = [code]; } for (let c of code) { if (this.released(c)) { let currKeyTime = this.#_getValueWithCode(c, (c) => this._prevLastMousePressedTime[c], (c) => this._prevLastKeyPressedTime[c], this._prevLastTouchPressedTime, this._prevLastCustomPressedTime); if (currTime - currKeyTime <= maxInterval) { return true; } } } return false; } /** * Get mouse wheel sign. * @returns {Number} Mouse wheel sign (-1 or 1) for wheel scrolling that happened during this frame. * Will return 0 if mouse wheel is not currently being used. */ get mouseWheelSign() { return Math.sign(this._mouseWheel); } /** * Get mouse wheel value. * @returns {Number} Mouse wheel value. */ get mouseWheel() { return this._mouseWheel; } /** * @inheritdoc * @private **/ endFrame() { // set mouse previous position and clear mouse move cache this._mousePrevPos = this._mousePos.clone(); // reset pressed / release events this._keyboardPressed = {}; this._keyboardReleased = {}; this._mousePressed = {}; this._mouseReleased = {}; this._customPressed = {}; this._customReleased = {}; // reset touch start / end states this._touchStarted = false; this._touchEnded = false; // reset mouse wheel this._mouseWheel = 0; } /** * Get keyboard key code from event. * @private */ #_getKeyboardKeyCode(event) { event = this._getEvent(event); return event.keyCode !== undefined ? event.keyCode : event.key.charCodeAt(0); } /** * Called when window loses focus - clear all input states to prevent keys getting stuck. * @private */ _onBlur(event) { if (this.resetOnFocusLoss) { this.#_resetAll(true); } } /** * Handle mouse wheel events. * @private * @param {*} event Event data from browser. */ _onMouseWheel(event) { this._mouseWheel = event.deltaY; } /** * Handle keyboard down event. * @private * @param {*} event Event data from browser. */ _onKeyDown(event) { var keycode = this.#_getKeyboardKeyCode(event); if (!this._keyboardState[keycode]) { this._keyboardPressed[keycode] = true; this._prevLastKeyPressedTime[keycode] = this._lastKeyPressedTime[keycode]; this._lastKeyPressedTime[keycode] = timestamp(); } this._keyboardState[keycode] = true; } /** * Handle keyboard up event. * @private * @param {*} event Event data from browser. */ _onKeyUp(event) { var keycode = this.#_getKeyboardKeyCode(event) || 0; this._keyboardState[keycode] = false; this._keyboardReleased[keycode] = true; this._prevLastKeyReleasedTime[keycode] = this._lastKeyReleasedTime[keycode]; this._lastKeyReleasedTime[keycode] = timestamp(); } /** * Extract position from touch event. * @private * @param {*} event Event data from browser. * @returns {Vector2} Position x,y or null if couldn't extract touch position. */ _getTouchEventPosition(event) { var touches = event.changedTouches || event.touches; if (touches && touches.length) { var touch = touches[0]; var x = touch.pageX || touch.offsetX || touch.clientX; var y = touch.pageY || touch.offsetY || touch.clientY; return new Vector2(x, y); } return null; } /** * Handle touch start event. * @private * @param {*} event Event data from browser. */ _onTouchStart(event) { // update position let position = this._getTouchEventPosition(event); if (position) { if (this.delegateTouchInputToMouse) { this._mousePos.x = position.x; this._mousePos.y = position.y; this._normalizeMousePos(); } } // set touching flag this._isTouching = true; this._touchStarted = true; // update time this._prevLastTouchPressedTime = this._lastTouchPressedTime; this._lastTouchPressedTime = timestamp(); // mark that touch started if (this.delegateTouchInputToMouse) { this._mouseButtonDown(this.MouseButtons.left); } } /** * Handle touch end event. * @private * @param {*} event Event data from browser. */ _onTouchEnd(event) { // update position let position = this._getTouchEventPosition(event); if (position) { this._touchPosition.copy(position); if (this.delegateTouchInputToMouse) { this._mousePos.x = position.x; this._mousePos.y = position.y; this._normalizeMousePos(); } } // clear touching flag this._isTouching = false; this._touchEnded = true; // update touch end time this._prevLastTouchReleasedTime = this._lastTouchReleasedTime; this._lastTouchReleasedTime = timestamp(); // mark that touch ended if (this.delegateTouchInputToMouse) { this._mouseButtonUp(this.MouseButtons.left); } } /** * Handle touch move event. * @private * @param {*} event Event data from browser. */ _onTouchMove(event) { // update position let position = this._getTouchEventPosition(event); if (position) { this._touchPosition.copy(position); if (this.delegateTouchInputToMouse) { this._mousePos.x = position.x; this._mousePos.y = position.y; this._normalizeMousePos(); } } // set touching flag this._isTouching = true; } /** * Handle mouse down event. * @private * @param {*} event Event data from browser. */ _onMouseDown(event) { event = this._getEvent(event); if (this.disableMouseWheelAutomaticScrolling && (event.button === this.MouseButtons.middle)) { event.preventDefault(); } this._mouseButtonDown(event.button); } /** * Handle mouse up event. * @private * @param {*} event Event data from browser. */ _onMouseUp(event) { event = this._getEvent(event); this._mouseButtonUp(event.button); } /** * Mouse button pressed logic. * @private * @param {*} button Button pressed. */ _mouseButtonDown(button) { this._mouseState[button] = true; this._mousePressed[button] = true; this._prevLastMousePressedTime[button] = this._lastMousePressedTime[button]; this._lastMousePressedTime[button] = timestamp(); } /** * Mouse button released logic. * @private * @param {*} button Button released. */ _mouseButtonUp(button) { this._mouseState[button] = false; this._mouseReleased[button] = true; this._prevLastMouseReleasedTime[button] = this._lastMouseReleasedTime[button]; this._lastMouseReleasedTime[button] = timestamp(); } /** * Handle mouse move event. * @private * @param {*} event Event data from browser. */ _onMouseMove(event) { // get event in a cross-browser way event = this._getEvent(event); // try to get position from event with some fallbacks var pageX = event.clientX; if (pageX === undefined) { pageX = event.x; } if (pageX === undefined) { pageX = event.offsetX; } if (pageX === undefined) { pageX = event.pageX; } var pageY = event.clientY; if (pageY === undefined) { pageY = event.y; } if (pageY === undefined) { pageY = event.offsetY; } if (pageY === undefined) { pageY = event.pageY; } // if pageX and pageY are not supported, use clientX and clientY instead if (pageX === undefined) { pageX = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; pageY = event.clientY + document.body.scrollTop + document.documentElement.scrollTop; } // set current mouse position this._mousePos.x = pageX; this._mousePos.y = pageY; this._normalizeMousePos(); } /** * Normalize current _mousePos value to be relative to target element. * @private */ _normalizeMousePos() { if (this._targetElement && this._targetElement.getBoundingClientRect) { var rect = this._targetElement.getBoundingClientRect(); this._mousePos.x -= rect.left; this._mousePos.y -= rect.top; } this._mousePos.roundSelf(); } /** * Get event either from event param or from window.event. * This is for older browsers support. * @private */ _getEvent(event) { return event || window.event; } } // export main object module.exports = new Input();