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"
];