export const useGamepadStore = defineStore("gamepad-store", () => { const styleStore = useStyleStore(); const fullScreenSet = getFullScreenSet(); var cursorSpeedTimeout = null; /** These are the gamepad cursors that can be used with the website. */ const gamepadCursors = [useGamepadCursor(0), useGamepadCursor(1), useGamepadCursor(2), useGamepadCursor(3)]; const showCursorSpeedMenu = ref(false); const gamepadConnected = computed(() => { return (-1 != gamepadCursors.findIndex(item => item.connected.value)); }); const cursorVisible = computed(() => { return (-1 != gamepadCursors.findIndex(item => item.showCursor.value)); }); const maxSpeedChanging = computed(() => { return (-1 != gamepadCursors.findIndex(item => item.maxSpeedChanging.value)); }); const cursorElementTitle = computed(() => { const index = gamepadCursors.findIndex(item => (item.elementTitle.value !== "")); return ((index == -1) ? "" : getCursor(index).elementTitle.value); }); // This hides the website cursor when a gamepad is being used. watch(cursorVisible, (newValue) => { styleStore.setHideCursorArray(1, newValue); }); // This adds a delay when the cursor speed menu is closing. watch(maxSpeedChanging, () => { if(cursorSpeedTimeout != null) { clearTimeout(cursorSpeedTimeout); } if(maxSpeedChanging.value) { showCursorSpeedMenu.value = true; } else { cursorSpeedTimeout = setTimeout(() => { showCursorSpeedMenu.value = false; }, 1000); } }); /** This returns an object representing a gamepad cursor. */ function getCursor(index) { return ((index < 0 || index >= gamepadCursors.length) ? null : gamepadCursors[index]); } /** This function runs whenever the visitor clicks on a typical gamepad "menu" button. */ function onGamepadMenuClick() { if(fullScreenSet.value && document.fullscreenElement !== document.body) { return; } useWebsiteDataStore().toggleNavMenu(); triggerClickSound(); } /** This function runs whenever the visitor clicks on "x", "y", or the right stick. */ function onScrollToTopButton() { useScrollStore().gamepadScrollToTop(); triggerClickSound(); } /** This function hides all the gamepad cursors on the website. */ function resetCursorPositions() { for(let i = 0; i < gamepadCursors.length; i++) { gamepadCursors[i].initCursorPosition(); } } /** This function hides all the gamepad cursors on the website. */ function hideAllCursors() { for(let i = 0; i < gamepadCursors.length; i++) { gamepadCursors[i].setCustomCursor(false); } } /** This function fully stops all the gamepad cursors on the website. */ function stopAllCursors() { for(let i = 0; i < gamepadCursors.length; i++) { gamepadCursors[i].stop(); } } return { gamepadCursors, gamepadConnected, cursorElementTitle, showCursorSpeedMenu, getCursor, onGamepadMenuClick, onScrollToTopButton, resetCursorPositions, hideAllCursors, stopAllCursors }});/** * This function creates a Gamepad Cursor object for the Gamepad Store to use. * @param {Number} index The index of the gamepad object. */function useGamepadCursor(index = 0) { const webData = useWebsiteDataStore(); const scrollStore = useScrollStore(); const windowSize = useMohitWindowSize(); const color = ref(CUSTOM_CURSOR_COLORS[index]); var cursorAnimationFrameId = null; const connected = ref(false); const connectedFresh = ref(false); const standardMapping = ref(true); const showCursor = ref(false); const maxSpeedChanging = ref(false); const maxSpeed = ref(10); const x = ref(0); const y = ref(0); /** @type {Ref<HTMLElement>} This is the element that the cursor is hovering over that can be clicked on. */ const clickElement = ref(null); const onElement = computed(() => { return (clickElement.value != null); }); // This detects whether the custom cursor is on an input or text area element. const onInputElement = computed(() => { const element = clickElement.value; return (onElement.value && (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)); }); const style = computed(() => { return { left: (String(x.value) + "px"), top: (String(y.value) + "px"), fontSize: (onElement.value ? '32px' : ''), color: color.value }}); const icon = computed(() => { return (onElement.value ? (onInputElement.value ? 'fa-i-cursor' : 'fa-hand-pointer') : 'fa-arrow-pointer'); }); const animation = computed(() => { return (onElement.value ? ['animate__animated', 'animate__pulse', 'animate__infinite'] : []); }); const elementTitle = computed(() => { const element = clickElement.value; return (onElement.value ? (element instanceof HTMLAnchorElement ? element.href : element.title) : ''); }); // This will change connectedFresh based on when the connection status of the gamepad changes. watch(connected, () => { if(!connected.value) { connectedFresh.value = false; } else { connectedFresh.value = true; setTimeout(() => { connectedFresh.value = false; }, 3000); } }); /** * This function starts animation frames for checking if any gamepad is connected or not. */ function start() { if(cursorAnimationFrameId != null) { return; } const checkGamepadConnected = () => { if(checkGamepadsSupported()) { const gamepads = navigator.getGamepads(); connected.value = (gamepads[index] && gamepads[index].connected); if(connected.value) { if(showCursor.value) { setClickElement(); } setStandardMapping(gamepads[index].mapping === "standard"); cursorAnimationFrameId = requestAnimationFrame(checkGamepadConnected); } else { setStandardMapping(); stop(); } } else { connected.value = false; setStandardMapping(); stop(); } } cursorAnimationFrameId = requestAnimationFrame(checkGamepadConnected); } /** * This function stops the Gamepad Cursor object from updating. */ function stop() { if(cursorAnimationFrameId == null) { return; } cancelAnimationFrame(cursorAnimationFrameId); setCustomCursor(false); cursorAnimationFrameId = null; } /** * This function emits a click event at the cursor's location. */ function emitClick() { const element = clickElement.value; if(element == null) { document.body.click(); // Clicks on the document body if there is no button detected. } else { (onInputElement.value ? element.focus() : element.click()); nextTick(() => { setClickElement(); }); } } /** * This function sets the max number of pixels that the cursor can travel per millisecond. * @param {Number} newMax The max speed to set the cursor to. Default Value is 10. */ function setMaxCursorSpeed(newMax = 10) { maxSpeed.value = Math.min(30, Math.max(newMax, 1)); } /** * This function adds a value to the total cursor max speed. Use only with Gamepad Event. * @param {Number} amount The amount to add to the cursor speed. */ function addToMaxCursorSpeed(amount = 1) { maxSpeedChanging.value = true; setMaxCursorSpeed(maxSpeed.value + amount); } /** * This function indicates the user has stopped changing their max cursor speed. */ function stopChangingMaxCursorSpeed() { maxSpeedChanging.value = false } /** * This function sets the visibility of the custom cursor. * @param {Boolean} reset If true, this function displays the cursor, else it hides the cursor. */ function setCustomCursor(visible = false) { // document.body.style.cursor = (visible ? "none" : ""); showCursor.value = visible; if(!visible) { clickElement.value = null; } } /** * This sets whether or not the mapping is standard or not. * @param {Boolean} newStatus the new status of the mapping. Default Value is True. */ function setStandardMapping(newStatus = true) { standardMapping.value = newStatus; } /** * This finds the element that the cursor is hovering over. */ function setClickElement() { // This section stops the function if the gamepad or cursor is not enabled. if(!connected.value || !showCursor.value) { clickElement.value = null; return; } // This section finds the clickable element the custom cursor is on. const xVal = ((x.value + 15) / windowSize.cssToWindowWidthRatio.value); const yVal = ((y.value + 15) / windowSize.cssToWindowHeightRatio.value); const foundElement = document.elementFromPoint(xVal, yVal); const usuableLink = foundElement?.closest('a'); const usuableButton = foundElement?.closest('button'); const usuableTextArea = foundElement?.closest('textarea'); var usuableInput = foundElement?.closest('input'); usuableInput = ((usuableInput && usuableInput.type !== "range") ? usuableInput : undefined); // This section assigns the clickable element to a reference object. if(usuableLink) { clickElement.value = usuableLink; } else if(usuableButton) { clickElement.value = usuableButton; } else if(usuableInput) { clickElement.value = usuableInput; } else if(usuableTextArea) { clickElement.value = usuableTextArea; } else { clickElement.value = null; } } /** * This function initializes the custom cursor position. */ function initCursorPosition() { const xOffset = 30 * ((index % 2 == 1) ? -1 : 1); const yOffset = 30 * ((index < 3) ? -1 : 1); x.value = (windowSize.width.value / 2) + xOffset; y.value = (windowSize.height.value / 2) + yOffset; if(showCursor.value) { setClickElement(); } } /** * This function manages the custom cursor based on an event. * @param {{ stick: String, axisIndex: Number, movement: Number }} event The object returned by moving the axis. */ function manageCursor(event) { if(event.stick !== "left_stick") { return; } setCustomCursor(true); if(event.axisIndex == 0) { x.value += (maxSpeed.value * event.movement); if(x.value < -5) { x.value = -5; } if(x.value > (windowSize.width.value - 20)) { x.value = (windowSize.width.value - 20); } } else { y.value += (maxSpeed.value * event.movement); if(y.value < -5) { y.value = -5; } if(y.value > (windowSize.height.value - 20)) { y.value = (windowSize.height.value - 20); } } setClickElement(); } /** * This function manages the custom cursor based on what button the user pressed on their gamepad. * @param {Number} directionIndex The direction to move the cursor based on a number. Up is 0, Down is 1, Left is 2, and Right is 3. */ function manageCursorWithDpad(directionIndex = 0) { setCustomCursor(true); if(directionIndex > 1) { x.value += (maxSpeed.value * ((directionIndex == 2) ? -0.75 : 0.75)); if(x.value < 0) { x.value = 0; } if(x.value > (windowSize.width.value - 35)) { x.value = (windowSize.width.value - 35); } } else { y.value += (maxSpeed.value * ((directionIndex == 0) ? -0.75 : 0.75)); if(y.value < 0) { y.value = 0; } if(y.value > (windowSize.height.value - 35)) { y.value = (windowSize.height.value - 35); } } setClickElement(); } /** * This function will scroll on the page vertically depending on which button is held down. * @param {String} direction The direction to scroll. * @param {Number} speed The number of pixels to scroll. */ function initScrollYBy(direction = 'top', speed = 10) { const scrollElement = getScrollElement(); const scrollValue = (speed * ((direction === "top") ? -1 : 1)); if(scrollElement == undefined) { scrollStore.scrollByIncrement(scrollValue); } else { scrollElement.scrollBy(0, scrollValue); } } /** * This function checks whether the cursor is in the main navigation menu or not. */ function getScrollElement() { const navMenu = document.getElementById("mohit-navMenu"); if(!webData.navMenuOpen || navMenu == null) { return undefined; } const rect = navMenu.getBoundingClientRect(); const scrollable = (navMenu.scrollHeight > rect.height); if(!scrollable) { return undefined; } const xVal = x.value; const yVal = y.value; return ((xVal >= rect.left && xVal <= rect.right && yVal >= rect.top && yVal <= rect.bottom) ? navMenu : undefined); } return { index, color, connected, connectedFresh, standardMapping, showCursor, maxSpeedChanging, maxSpeed, x, y, clickElement, onElement, onInputElement, style, icon, animation, elementTitle, start, stop, emitClick, setClickElement, initCursorPosition, setMaxCursorSpeed, addToMaxCursorSpeed, stopChangingMaxCursorSpeed, setCustomCursor, manageCursor, manageCursorWithDpad, initScrollYBy }}/** This function returns whether the Web Gamepad API is supported or not. */export function checkGamepadsSupported() { return Boolean((typeof navigator !== 'undefined') && (typeof navigator.getGamepads === 'function'));}const CUSTOM_CURSOR_COLORS = [ "#FF4040", "#4169E1", "#FFBF00", "#2E8B57"];