export function snapToGrid(value: number, gridSize: number = 10) { return Math.round(value / gridSize) * gridSize; } export function lerp(a: number, b: number, t: number) { return a + (b - a) * t; } export function animate(duration: number, callback: (progress: number) => void | false) { const start = performance.now(); const loop = (time: number) => { const progress = (time - start) / duration; if (progress < 1) { const res = callback(progress); if (res !== false) { requestAnimationFrame(loop); } } else { callback(1); } } requestAnimationFrame(loop); } export function createNodePath({ depth = 8, height = 20, y = 50, cornerTop = 0, cornerBottom = 0, leftBump = false, rightBump = false, aspectRatio = 1, } = {}) { return `M0,${cornerTop} ${cornerTop ? ` V${cornerTop} Q0,0 ${cornerTop * aspectRatio},0 H${100 - cornerTop * aspectRatio} Q100,0 100,${cornerTop} ` : ` V0 H100 ` } V${y - height / 2} ${rightBump ? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}` : ` H100` } ${cornerBottom ? ` V${100 - cornerBottom} Q100,100 ${100 - cornerBottom * aspectRatio},100 H${cornerBottom * aspectRatio} Q0,100 0,${100 - cornerBottom} ` : `${leftBump ? `V100 H0` : `V100`}` } ${leftBump ? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}` : ` H0` } Z`.replace(/\s+/g, " "); } export const debounce = (fn: Function, ms = 300) => { let timeoutId: ReturnType; return function (this: any, ...args: any[]) { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn.apply(this, args), ms); }; }; export const clone: (v: T) => T = "structedClone" in globalThis ? globalThis.structuredClone : (obj) => JSON.parse(JSON.stringify(obj)); export const createLogger = (() => { let maxLength = 5; return (scope: string) => { maxLength = Math.max(maxLength, scope.length); let muted = false; let isGrouped = false; function s(color: string, ...args: any) { return isGrouped ? [...args] : [`[%c${scope.padEnd(maxLength, " ")}]:`, `color: ${color}`, ...args]; } return { log: (...args: any[]) => !muted && console.log(...s("#888", ...args)), info: (...args: any[]) => !muted && console.info(...s("#888", ...args)), warn: (...args: any[]) => !muted && console.warn(...s("#888", ...args)), error: (...args: any[]) => console.error(...s("#f88", ...args)), group: (...args: any[]) => { if (!muted) { console.groupCollapsed(...s("#888", ...args)); isGrouped = true; } }, groupEnd: () => { if (!muted) { console.groupEnd(); isGrouped = false } }, mute() { muted = true; }, unmute() { muted = false; } } } })(); export function withSubComponents>( component: A, subcomponents: B ): A & B { Object.keys(subcomponents).forEach((key) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (component as any)[key] = (subcomponents as any)[key]; }); return component as A & B; }