import { easeInOutQuart } from './easing' /** * Throttle function */ export const throttle = (func: Function, timeout: number) => { let ready: boolean = true return (...args: any) => { if (!ready) return ready = false func(...args) setTimeout(() => ready = true, timeout) } } /** * Debounce function */ export const debounce = (func: Function, timeout: number) => { let timer: NodeJS.Timeout return (...args: any) => { clearTimeout(timer) timer = setTimeout(() => func(...args), timeout) } } /** * Split text * @description Split a string into words or characters * @returns string[] */ export const splitText = (text: string, mode: string = 'words'): string[] => { // Split by words if (mode === 'words') { const words = text .replace(/\\n/g, '\n') .replace(/\s+/g, m => m.includes('\n') ? '\n ' : ' ') .trim() .split(' ') return words } // Split by chars else if (mode === 'chars') { const chars = Array.from(text).map(char => char === ' ' ? '\xa0' : char) return chars } } /** * Capitalize first letter */ export const capitalizeFirstLetter = (string: string) => { return string[0].toUpperCase() + string.slice(1) } /** * Create a delay */ export function sleep (milliseconds: number) { return new Promise(resolve => setTimeout(resolve, milliseconds)) } /** * Linear Interpolation */ export const lerp = (start: number, end: number, amount: number): number => { return (1 - amount) * start + amount * end } /** * Re-maps a number from one range to another * @param value the incoming value to be converted * @param start1 lower bound of the value's current range * @param stop1 upper bound of the value's current range * @param start2 lower bound of the value's target range * @param stop2 upper bound of the value's target range * @param [withinBounds] constrain the value to the newly mapped range * @return remapped number */ export const map = (n: number, start1: number, stop1: number, start2: number, stop2: number, withinBounds: boolean): number => { const value = (n - start1) / (stop1 - start1) * (stop2 - start2) + start2 if (!withinBounds) return value if (start2 < stop2) { return clamp(value, start2, stop2) } else { return clamp(value, stop2, start2) } } /** * Clamp a number */ export const clamp = (num: number, a: number, b: number) => { return Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b)) } /** * Return a random element from an array */ export const getRandomItem = (array: T[]): T => { const randomItemIndex = ~~(array.length * Math.random()) return array[randomItemIndex] } /** * Return random elements from an array */ export const getRandomItems = (array: any[], amount: number): T[] => { const shuffled = Array.from(array).sort(() => 0.5 - Math.random()) return shuffled.slice(0, amount) } /** * Get a DOM element's position */ export const getPosition = (node: any, scope?: HTMLElement) => { const root = scope || document let offsetTop = node.offsetTop let offsetLeft = node.offsetLeft while (node && node.offsetParent && node.offsetParent != document && node !== root && root !== node.offsetParent) { offsetTop += node.offsetParent.offsetTop offsetLeft += node.offsetParent.offsetLeft node = node.offsetParent } return { top: offsetTop, left: offsetLeft } } /** * Scroll back to top after page transition */ export const scrollToTop = (delay?: number) => { const scroll = () => window.scrollTo(0,0) if (delay && delay > 0) { setTimeout(scroll, delay) } else { scroll() } } /** * Smooth Scroll to an element * @description Promised based * @url Based on: https://www.youtube.com/watch?v=oUSvlrDTLi4 */ const smoothScrollPromise = (target: HTMLElement, duration: number = 1600): Promise => { const position = target.getBoundingClientRect().top + 1 const startPosition = window.scrollY const distance = position - startPosition let startTime: number = null // Return Promise return new Promise((resolve) => { if (!(target instanceof Element)) throw new TypeError('Argument 1 must be an Element') if (typeof window === 'undefined') return // Scroll to animation const animation = (currentTime: number) => { if (startTime === null) startTime = currentTime const timeElapsed = currentTime - startTime // Create easing value const easedYPosition = easeInOutQuart(timeElapsed, startPosition, distance, duration) // Scroll to Y position window.scrollTo(0, easedYPosition) // Loop or end animation if (timeElapsed < duration) { requestAnimationFrame(animation) } else { return resolve() } } requestAnimationFrame(animation) }) } export const smoothScroll = async ({ hash, callback, changeHash = true, event }: smoothScrollOptions) => { if (event) event.preventDefault() const target = document.getElementById(hash) smoothScrollPromise(target).then(() => { if (changeHash) location.hash = hash callback && callback() }) } type smoothScrollOptions = { hash: string changeHash?: boolean event?: MouseEvent callback?: Function }