import React, { useState } from "react"; import useLocation from "wouter/use-location"; export const sleep = (t: number) => new Promise(r => setTimeout(r, t)); type Ref = | React.MutableRefObject | React.RefCallback | React.ForwardedRef; export const composeRefs = (...refs: Ref[]) => { return (el: T) => { refs.forEach(ref => { if (typeof ref === "function") ref(el); else if (ref) ref.current = el; }); }; }; export function* intersperse( xs: T[], delim: (index: number) => U, ): Generator { let first = true; let i = 0; for (const x of xs) { if (!first) { yield delim(i); i++; } first = false; yield x; i++; } } export const getTimeout = () => { const clearables = new Set>(); const timeout = (f: (...attr: any[]) => any, t: number) => { const self = setTimeout(() => (f(), clearables.delete(self)), t); clearables.add(self); }; const clearTimers = () => { clearables.forEach(timer => clearTimeout(timer)); clearables.clear(); }; return [timeout, clearTimers] as const; }; export function debounce void>( func: Fn, wait: number, ): Fn { let timeoutId: number | null = null; return function (this: ThisParameterType, ...args: Parameters) { if (timeoutId !== null) clearTimeout(timeoutId); timeoutId = window.setTimeout(() => { func.apply(this, args); timeoutId = null; }, wait); } as Fn; } export const throttle = void>( fn: Fn, wait: number, ): Fn => { let inThrottle = false; let lastFn: ReturnType | undefined = undefined; let lastTime = 0; return function (this: ThisParameterType, ...args: Parameters) { const context = this; if (!inThrottle) { fn.apply(context, args); lastTime = Date.now(); inThrottle = true; } else { clearTimeout(lastFn); lastFn = setTimeout(function () { if (Date.now() - lastTime >= wait) { fn.apply(context, args); lastTime = Date.now(); } }, Math.max(wait - (Date.now() - lastTime), 0)); } } as Fn; }; export const ellipses = (text: string, length: number = 100) => text.length > length ? text.slice(0, length - 3) + "..." : text; export const useNav = () => { const [location, navigate] = useLocation(); return [ location, (link: string) => (e: React.MouseEvent | KeyboardEvent) => { e?.preventDefault(); if (e.ctrlKey) return window.open(link, "_blank", "noreferrer noopener"); navigate(link); }, ] as const; }; export function rewriteExtn(filename: string, extn: string) { const split = filename.split("."); split[split.length - 1] = extn; return split.join("."); } export function normalise(path: string) { return ( (path.startsWith("/") ? "/" : "") + path.trim().split("/").filter(Boolean).join("/") ); } export function comparePaths(p1: string, p2: string) { return normalise(p1) === normalise(p2); } // required css is inlined in index.html export function setupCursorTracking(el: HTMLElement | null) { if (!el) return; el.addEventListener("mousemove", e => { const rect = el.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; el.style.setProperty("--x", x + "px"); el.style.setProperty("--y", y + "px"); }); } export function clamp(value: number, min: number, max: number) { return Math.min(Math.max(value, min), max); } export function normaliseAngleDifference(delta: number): number { // Bring into range (-2PI, 2PI) delta = delta % (2 * Math.PI); if (delta > Math.PI) delta -= 2 * Math.PI; else if (delta <= -Math.PI) delta += 2 * Math.PI; return delta; } export interface Toggle { on: boolean; toggle: () => void; set: (value: boolean) => void; } export function useToggle(initial: boolean): Toggle { const [state, setState] = useState(initial); return { on: state, toggle: () => setState(s => !s), set: (value: boolean) => setState(value), }; }