import { Context } from "./context"; export const Directions = { LEFT: -1, RIGHT: 1, }; let debugMode = location.hostname === "127.0.0.1"; /** @type {Context|null} */ let context = null; /** * @returns {boolean} Whether debug mode is enabled */ export function isDebug() { return debugMode; } /** * @param {boolean} value */ export function setDebug(value) { debugMode = value; } /** * @returns {Context} The specific context for this platform */ export function getContext() { if (!context) { throw new Error("Context requested before being set"); } return context; } /** * @param {Context} newContext */ export function setContext(newContext) { context = newContext; } /** * Create an HTML element with the specified parameters * @param {string} className * @param {string} [textContent] * @param {string} [id] * @returns {HTMLElement} */ export function makeElement(className, textContent, id) { const element = document.createElement("div"); element.classList.add(className); if (textContent) { element.textContent = textContent; } if (id) { element.id = id; } return element; } /** * @param {Document|Element} element * @param {(e: Event) => void} action */ export function onClick(element, action) { element.addEventListener("click", (e) => action(e)); element.addEventListener("touchend", (e) => { if (e instanceof TouchEvent === false) { return; } else if (element instanceof HTMLElement === false) { return; } const touch = e.changedTouches[0]; const rect = element.getBoundingClientRect(); if ( touch.clientX >= rect.left && touch.clientX <= rect.right && touch.clientY >= rect.top && touch.clientY <= rect.bottom ) { action(e); } }); } /** * @param {HTMLElement|null} element The element to detect drag events on * @param {boolean} [parent] Whether to move the parent element when the child is dragged * @param {(top: number, left: number) => void} [callback] Callback for when element is moved * @param {HTMLElement} [pageElement] The page element to constrain movement within */ export function makeDraggable(element, parent = true, callback = () => { }, pageElement) { if (!element) { return; } let isMouseDown = false; let offsetX = 0; let offsetY = 0; let elementToMove = parent ? element.parentElement : element; if (!elementToMove) { error("Birb: Parent element not found"); return; } element.addEventListener("mousedown", (e) => { isMouseDown = true; offsetX = e.clientX - elementToMove.offsetLeft; offsetY = e.clientY - elementToMove.offsetTop; }); element.addEventListener("touchstart", (e) => { isMouseDown = true; const touch = e.touches[0]; offsetX = touch.clientX - elementToMove.offsetLeft; offsetY = touch.clientY - elementToMove.offsetTop; e.preventDefault(); e.stopPropagation(); }); document.addEventListener("mouseup", (e) => { if (isMouseDown) { callback(elementToMove.offsetTop, elementToMove.offsetLeft); e.preventDefault(); } isMouseDown = false; }); document.addEventListener("touchend", (e) => { if (isMouseDown) { callback(elementToMove.offsetTop, elementToMove.offsetLeft); e.preventDefault(); } isMouseDown = false; }); document.addEventListener("mousemove", (e) => { const page = pageElement || document.documentElement; const maxX = page.scrollWidth - elementToMove.clientWidth; const maxY = page.scrollHeight - elementToMove.clientHeight; if (isMouseDown) { elementToMove.style.left = `${Math.max(0, Math.min(maxX, e.clientX - offsetX))}px`; elementToMove.style.top = `${Math.max(0, Math.min(maxY, e.clientY - offsetY))}px`; } }); document.addEventListener("touchmove", (e) => { if (isMouseDown) { const touch = e.touches[0]; elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; } }); } /** * @param {() => void} func * @param {Element} [closeButton] * @param {boolean} [allowEscape] Whether to allow closing with the Escape key */ export function makeClosable(func, closeButton, allowEscape = true) { if (closeButton) { onClick(closeButton, func); } document.addEventListener("keydown", (e) => { if (closeButton && !document.body.contains(closeButton)) { return; } if (allowEscape && e.key === "Escape") { func(); } }); } /** * @returns {boolean} Whether the user is on a mobile device */ export function isMobile() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); } export function log() { console.log("Birb: ", ...arguments); } export function debug() { if (isDebug()) { console.debug("Birb: ", ...arguments); } } export function error() { console.error("Birb: ", ...arguments); } /** * Get a layer from a sprite sheet array * @param {string[][]} spriteSheet The sprite sheet pixel array * @param {number} spriteIndex The sprite index * @param {number} width The width of each sprite * @returns {string[][]} */ export function getLayerPixels(spriteSheet, spriteIndex, width) { // From an array of a horizontal sprite sheet, get the layer for a specific sprite const layer = []; for (let y = 0; y < width; y++) { layer.push(spriteSheet[y].slice(spriteIndex * width, (spriteIndex + 1) * width)); } return layer; } /** * The height of the inner browser window * Will be the same as getFixedWindowHeight() on most browsers * On iOS, it will vary to be the height excluding the current address bar size (potentially greater than fixed height) */ export function getWindowHeight() { // Necessary because iOS 26 Safari is terrible and won't render // fixed/sticky elements behind the address bar return window.innerHeight; } /** * The fixed height of the inner browser window * Will be the same as getWindowHeight() on most browsers * On iOS, it will always be the height of the window when the address bar is fully expanded * @returns The true height of the inner browser window */ export function getFixedWindowHeight() { return document.documentElement.clientHeight; }