Files
Pocket-Bird/src/shared.js
2026-03-08 12:47:08 -07:00

233 lines
6.0 KiB
JavaScript

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;
}