├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── arrayPick.ts ├── arrayShuffle.ts ├── clamp.ts ├── damp.ts ├── debounce.ts ├── distance.ts ├── fract.ts ├── index.ts ├── inertia.ts ├── intersectionObserver.ts ├── inverseLerp.ts ├── lerp.ts ├── map.ts ├── objectPool.ts ├── pipe.ts ├── prefetch.ts ├── prng.ts ├── raf.ts ├── random.ts ├── randomGaussian.ts ├── react │ ├── index.ts │ ├── useRaf.ts │ ├── useScroll.ts │ └── useScrollProgress.ts ├── round.ts ├── scroll.ts ├── smoothClamp.ts ├── smootherstep.ts ├── smoothstep.ts ├── spring.ts ├── stateMachine.ts └── weightedList.ts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Maximilian Berndt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **arrayPick** Randomly pick a value from an array 2 | 3 | ``` 4 | arrayPick([1,2,3]) // 2 5 | ``` 6 | 7 | **arrayShuffle** Randomly shuffle an array 8 | 9 | ``` 10 | arrayShuffle([1, 2, 3]) // [2, 1, 3] 11 | ``` 12 | 13 | **clamp** Clamps a value between an upper and lower bound. 14 | 15 | ``` 16 | const value = 100; 17 | clamp(value, 0, 10) // 10 18 | ``` 19 | 20 | **damp** Framerate independent lerp 21 | 22 | ``` 23 | const value = 10; 24 | damp(value, 50, 0.4, deltaTime) // 26 25 | ``` 26 | 27 | **debounce** Will only trigger function when it has not been invoked in given timeframe 28 | 29 | ``` 30 | const onResize = () => { console.log(window.innerWidth) } 31 | document.addEventListener("resize", debounce(onResize, 250)) // Will be called on resize every 250ms 32 | ``` 33 | 34 | **distance** Distance between two points using pythagoras theorem 35 | 36 | ``` 37 | distance({ x: 0, y: 2 }, { x: 1, y: -2}) // 4.123 38 | ``` 39 | 40 | **stateMachine** 41 | 42 | ``` 43 | // Create states 44 | const walkState = state({ 45 | name: "walk" 46 | enter: () => console.log("enter"), 47 | }) 48 | const runState = state({ 49 | name: "run" 50 | exit: () => console.log("leave run"), 51 | update: () => console.log("update run"), 52 | }) 53 | 54 | // Create state machine 55 | const fsm = stateMachine([walkState, runState]) 56 | 57 | // Update on every tick 58 | raf.add(fsm.update) 59 | 60 | // Change current state 61 | fsm.setState("run") 62 | ``` 63 | 64 | **fract** Loop a value between 0 and 1 65 | 66 | ``` 67 | fract(10.2) // 0.2 68 | ``` 69 | 70 | **inertia** 71 | 72 | ``` 73 | // Create inertia value 74 | const value = inertia(0) 75 | 76 | rad.add(() => { 77 | value.update() 78 | 79 | // Get value 80 | consol.log(value.get()) 81 | }) 82 | 83 | window.addEventListner("mousemove", (e) => { 84 | // Add delta 85 | value.add(e.clientX - value.get()) 86 | }) 87 | ``` 88 | 89 | **intersectionObserver** 90 | 91 | ``` 92 | TODO: document 93 | ``` 94 | 95 | **lerp** Linear interpolation between two known points. 96 | 97 | ``` 98 | const value = 10; 99 | lerp(value, 50, 0.4) // 26 100 | ``` 101 | 102 | **map** Re-maps a number from one range to another. 103 | 104 | ``` 105 | const value = 5; 106 | map(value, 0, 10, 3, 20) // 11.5 107 | ``` 108 | 109 | **objectPool** Declare items once and then reuse them 110 | 111 | ``` 112 | const objects = objectPool([...Array(10)].map(new AnimatedLine)) 113 | objects.getNext() // will get the next object in the pool, once at the end, will get the first again 114 | ``` 115 | 116 | **pipe** Pass the result of one function to the next, creating a clean and readable pipeline 117 | 118 | ``` 119 | const add = (x) => x + 1; 120 | const multiply = (x) => x * 2; 121 | 122 | const result = pipe(add, multiply)(5); // 12 123 | ``` 124 | 125 | **prefetch** Prefectch a page, waits until the main thread is idle 126 | 127 | ``` 128 | prefetch(["/about.html", "/contact.html"]) 129 | ``` 130 | 131 | **prng** Generate a pseudo random number between 0 and 1 based on a seed 132 | 133 | ``` 134 | const generator = prng("Your seed") 135 | console.log(generator()) // 0.093 - will be same for every time the seed is called 136 | ``` 137 | 138 | **raf** Central request animation frame loop, that starts and cancels itself. 139 | 140 | ``` 141 | // Add function to raf 142 | const remove = raf(() => console.log("hello")) 143 | 144 | // Remove function again 145 | setTimeout(remove, 1000) 146 | ``` 147 | 148 | **random** Returns a random number in a given range. 149 | 150 | ``` 151 | const value = random(0, 100) 152 | ``` 153 | 154 | **randomBool** Randomly returns true or false 155 | 156 | ``` 157 | randomBool() 158 | ``` 159 | 160 | **randomInt** Returns a random integer in between two values 161 | 162 | ``` 163 | randomInt(0, 10) // 3 164 | ``` 165 | 166 | **randomGaussian** Random number that more likely returns the medium of the standard deviation to more closely mimic a natural randomness 167 | 168 | ``` 169 | randomGaussian(1) // Returns a value between 1 and -1 170 | ``` 171 | 172 | **round** Round to given decimal 173 | 174 | ``` 175 | const value = 10.4567890 176 | round(value, 1000) // 10.456 177 | ``` 178 | 179 | **smoothstep** 180 | 181 | ``` 182 | TODO: document 183 | ``` 184 | 185 | **smootherstep** 186 | 187 | ``` 188 | TODO: document 189 | ``` 190 | 191 | **spring** 192 | 193 | ``` 194 | // Create spring 195 | const spring = createSpring(0, { stiffness: 0.2, damping: 0.4, mass: 1.2 } ) 196 | 197 | rad.add(() => { 198 | // Update spring every tick 199 | spring.update() 200 | 201 | // Get value 202 | console.log(spring.get()) 203 | }) 204 | 205 | window.addEventListner("mousemove", (e) => { 206 | // Set target value 207 | spring.set(e.clientX) 208 | }) 209 | ``` 210 | 211 | **weightedList** 212 | 213 | ``` 214 | const list = weightedList([ 215 | { item: "Option 1", weight: 9 }, 216 | { item: "Option 2", weight: 1 } 217 | ]) 218 | 219 | list.get() // 9 out of 10 times will return "Option 1" 220 | ``` 221 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@maaax/utils", 3 | "version": "1.1.2", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@maaax/utils", 9 | "version": "1.1.2", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "typescript": "^5.1.6" 13 | } 14 | }, 15 | "node_modules/typescript": { 16 | "version": "5.3.3", 17 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", 18 | "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", 19 | "dev": true, 20 | "bin": { 21 | "tsc": "bin/tsc", 22 | "tsserver": "bin/tsserver" 23 | }, 24 | "engines": { 25 | "node": ">=14.17" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@maaax/utils", 3 | "version": "2.0.3", 4 | "description": "Collection of utility functions", 5 | "main": "./src/index.ts", 6 | "scripts": { 7 | "test": "test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/maximilianberndt/utils.git" 12 | }, 13 | "keywords": [ 14 | "utility" 15 | ], 16 | "author": "Maximilian Berndt", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/maximilianberndt/utils/issues" 20 | }, 21 | "homepage": "https://github.com/maximilianberndt/utils#readme", 22 | "devDependencies": { 23 | "typescript": "^5.4.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/arrayPick.ts: -------------------------------------------------------------------------------- 1 | export const arrayPick = (array: T[], index = Math.random()): T => 2 | array[Math.floor(index * array.length)] 3 | -------------------------------------------------------------------------------- /src/arrayShuffle.ts: -------------------------------------------------------------------------------- 1 | export const arrayShuffle = (array: T[]): T[] => { 2 | for (let i = array.length - 1; i > 0; i--) { 3 | const j = Math.floor(Math.random() * (i + 1)) 4 | ;[array[i], array[j]] = [array[j], array[i]] 5 | } 6 | 7 | return array 8 | } 9 | -------------------------------------------------------------------------------- /src/clamp.ts: -------------------------------------------------------------------------------- 1 | export const clamp = (v: number, a = 0, z = 1): number => 2 | Math.min(Math.max(v, a), z) 3 | -------------------------------------------------------------------------------- /src/damp.ts: -------------------------------------------------------------------------------- 1 | import { lerp } from './lerp' 2 | 3 | // http://www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/ 4 | export const damp = (a = 0, z = 0, deltaTime = 0, smoothing = 0.5) => 5 | lerp(a, z, 1 - Math.exp(-smoothing * deltaTime)) 6 | -------------------------------------------------------------------------------- /src/debounce.ts: -------------------------------------------------------------------------------- 1 | export const debounce = (fn: () => void, delayMS = 0) => { 2 | let dt 3 | return function () { 4 | const ctx = this 5 | const args = arguments 6 | clearTimeout(dt) 7 | 8 | dt = setTimeout(() => fn.apply(ctx, args), delayMS) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/distance.ts: -------------------------------------------------------------------------------- 1 | export type Point = { 2 | x: number 3 | y: number 4 | } 5 | 6 | export const distance = (p1: Point, p2: Point) => 7 | Math.hypot(p2.x - p1.x, p2.y - p1.y) 8 | -------------------------------------------------------------------------------- /src/fract.ts: -------------------------------------------------------------------------------- 1 | export const fract = (value: number) => value - Math.floor(value) 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { arrayPick } from './arrayPick' 2 | export { arrayShuffle } from './arrayShuffle' 3 | export { clamp } from './clamp' 4 | export { damp } from './damp' 5 | export { debounce } from './debounce' 6 | export { distance } from './distance' 7 | export { fract } from './fract' 8 | export { inertia } from './inertia' 9 | export { intersectionObserver } from './intersectionObserver' 10 | export { inverseLerp } from './inverseLerp' 11 | export { lerp } from './lerp' 12 | export { map } from './map' 13 | export { objectPool } from './objectPool' 14 | export { pipe } from './pipe' 15 | export { prefetch } from './prefetch' 16 | export { prng } from './prng' 17 | export { raf } from './raf' 18 | export { random, randomBool, randomInt } from './random' 19 | export { randomGaussian } from './randomGaussian' 20 | export { round } from './round' 21 | export { onScroll } from './scroll' 22 | export { smoothClamp } from './smoothClamp' 23 | export { smootherstep } from './smootherstep' 24 | export { smoothstep } from './smoothstep' 25 | export { createSpring } from './spring' 26 | export { state, stateMachine } from './stateMachine' 27 | export { weightedList } from './weightedList' 28 | -------------------------------------------------------------------------------- /src/inertia.ts: -------------------------------------------------------------------------------- 1 | export interface Inertia { 2 | update: (deltaTime: number) => void 3 | add: (delta: number) => void 4 | jump: (value: number) => void 5 | get: () => number 6 | setFriction: (friction: number) => void 7 | } 8 | 9 | 10 | export const inertia = ({ start = 0, friction = 0.97 }): Inertia => { 11 | const data = { 12 | value: start, 13 | velocity: 0, 14 | friction, 15 | } 16 | 17 | // Update every frame 18 | const update = (dt = 1 / 60) => { 19 | const timeAdjust = dt * 60 20 | const frameFriction = Math.pow(data.friction, timeAdjust) 21 | 22 | data.velocity *= frameFriction 23 | data.value += data.velocity * timeAdjust 24 | } 25 | 26 | // Directly update the current value and set velocity to 0 27 | const jump = (v = 0) => { 28 | data.value = v 29 | data.velocity = 0 30 | } 31 | 32 | return { 33 | update, 34 | get: () => data.value, 35 | jump, 36 | add: (delta = 0) => { 37 | data.velocity += delta 38 | }, 39 | setFriction: (friction = 0.97) => { 40 | data.friction = friction 41 | }, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/intersectionObserver.ts: -------------------------------------------------------------------------------- 1 | export const intersectionObserver = ({ 2 | el, 3 | onEnter, 4 | onLeave, 5 | once = false, 6 | options = {}, 7 | }: { 8 | el: HTMLElement 9 | onEnter?: () => void 10 | onLeave?: () => void 11 | once?: boolean 12 | options?: { 13 | threshold?: number | number[] 14 | rootMargin?: string 15 | } 16 | }): IntersectionObserver => { 17 | // Triggers every time an intersection happens 18 | const callback = ( 19 | entries: IntersectionObserverEntry[], 20 | observer: IntersectionObserver 21 | ): void => { 22 | if (entries[0].isIntersecting) { 23 | // Element comes into the viewport 24 | if (onEnter) onEnter() 25 | if (once) observer.unobserve(el) 26 | } else { 27 | // Element leaves the viewport 28 | if (onLeave) onLeave() 29 | } 30 | } 31 | 32 | const observer = new IntersectionObserver(callback, { 33 | threshold: 0, 34 | ...options, 35 | }) 36 | 37 | observer.observe(el) 38 | 39 | return observer 40 | } 41 | -------------------------------------------------------------------------------- /src/inverseLerp.ts: -------------------------------------------------------------------------------- 1 | export const inverseLerp = (a = 0, z = 1, value = 0.5) => 2 | (value - a) / (z - a) 3 | -------------------------------------------------------------------------------- /src/lerp.ts: -------------------------------------------------------------------------------- 1 | export const lerp = (a = 0, z = 1, t = 0) => a * (1 - t) + z * t 2 | -------------------------------------------------------------------------------- /src/map.ts: -------------------------------------------------------------------------------- 1 | export const map = (v = 0, a = 0, z = 1, b = -1, y = -1): number => 2 | b + (y - b) * ((v - a) / (z - a)) 3 | -------------------------------------------------------------------------------- /src/objectPool.ts: -------------------------------------------------------------------------------- 1 | type ObjectPool = { 2 | getNext: () => T 3 | get: () => T[] 4 | } 5 | 6 | export const objectPool = (data: T[] = []): ObjectPool => { 7 | let current = 0 8 | 9 | const updateCurrent = () => { 10 | current++ 11 | if (current === data.length) current = 0 12 | } 13 | 14 | const getNext = () => { 15 | const d = data[current] 16 | 17 | updateCurrent() 18 | 19 | return d 20 | } 21 | 22 | return { 23 | getNext, 24 | get: () => data, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/pipe.ts: -------------------------------------------------------------------------------- 1 | type UnaryFunction = (arg: T) => R 2 | 3 | export const pipe = 4 | (...fns: UnaryFunction[]): UnaryFunction => 5 | (input: T) => 6 | fns.reduce((prev, fn) => fn(prev), input) 7 | -------------------------------------------------------------------------------- /src/prefetch.ts: -------------------------------------------------------------------------------- 1 | // Prefetch subsequent pages 2 | // TODO: Dom parse and select preload critical images 3 | 4 | /** checks if your machine supports prefetching using link[rel="prefetch"] */ 5 | function support(feature) { 6 | const link = document.createElement('link') 7 | return ( 8 | (link.relList || {}).supports && link.relList.supports(feature) 9 | ) 10 | } 11 | 12 | export const prefetch = ( 13 | links: string[], 14 | prefetchMethod = 'prefetch' 15 | ) => { 16 | if (!links.length) return 17 | 18 | const supportedPrefetchStrategy = 19 | support('prefetch') && prefetchMethod == 'prefetch' 20 | ? linkPrefetchStrategy 21 | : xhrPrefetchStrategy 22 | 23 | const linksL = links.length 24 | 25 | requestIdleCallback( 26 | () => { 27 | for (let i = 0; i < linksL; i++) { 28 | prefetch(links[i]) 29 | } 30 | }, 31 | { timeout: 23 } 32 | ) 33 | 34 | /** prefetches a resource by using link[rel="prefetch"] 35 | * It creates a link element and appends attributes: rel="prefetch" and href with the url param as the value. 36 | */ 37 | function linkPrefetchStrategy(url) { 38 | return new Promise((resolve, reject) => { 39 | const link = document.createElement(`link`) 40 | link.rel = `prefetch` 41 | link.href = url 42 | link.onload = resolve 43 | link.onerror = reject 44 | document.head.appendChild(link) 45 | }) 46 | } 47 | 48 | /** prefetches a resource using XHR */ 49 | function xhrPrefetchStrategy(url) { 50 | return new Promise((resolve, reject) => { 51 | const req = new XMLHttpRequest() 52 | req.open(`GET`, url, (req.withCredentials = true)) 53 | req.onload = () => { 54 | if (req.status === 200) { 55 | let response = req.response 56 | 57 | // TODO: Dom parse and select preload critical images 58 | console.log(response) 59 | 60 | resolve(response) 61 | } else { 62 | reject() 63 | } 64 | } 65 | req.send() 66 | }) 67 | } 68 | 69 | /** prefetches a resource */ 70 | function prefetch(url) { 71 | url = new URL(url, location.href) 72 | let conn = navigator.connection 73 | 74 | if (!conn) return 75 | if (conn.effectiveType.includes('2g') || conn.saveData) return 76 | 77 | return supportedPrefetchStrategy(url).then( 78 | () => { 79 | console.log(` ${url} fetched`) 80 | }, 81 | () => { 82 | console.log(`${url} not fetched`) 83 | } 84 | ) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/prng.ts: -------------------------------------------------------------------------------- 1 | // Seed generating function from string 2 | // From https://github.com/bryc/code/blob/master/jshash/PRNGs.md#addendum-a-seed-generating-functions 3 | function xmur3(str = '') { 4 | for (var i = 0, h = 1779033703 ^ str.length; i < str.length; i++) 5 | (h = Math.imul(h ^ str.charCodeAt(i), 3432918353)), 6 | (h = (h << 13) | (h >>> 19)) 7 | return function () { 8 | ;(h = Math.imul(h ^ (h >>> 16), 2246822507)), 9 | (h = Math.imul(h ^ (h >>> 13), 3266489909)) 10 | return (h ^= h >>> 16) >>> 0 11 | } 12 | } 13 | 14 | // Generate random number based on seed 15 | // From https://stackoverflow.com/a/47593316 16 | function mulberry32(a) { 17 | return function () { 18 | a |= 0 19 | a = (a + 0x6d2b79f5) | 0 20 | var t = Math.imul(a ^ (a >>> 15), 1 | a) 21 | t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t 22 | return ((t ^ (t >>> 14)) >>> 0) / 4294967296 23 | } 24 | } 25 | 26 | export const prng = (seed = `${Date.now()}`) => 27 | mulberry32(xmur3(seed)()) 28 | -------------------------------------------------------------------------------- /src/raf.ts: -------------------------------------------------------------------------------- 1 | export type RafCallback = (t: number, deltaTime: number) => any 2 | 3 | let rafId = 0 4 | let now = 0 5 | let renderQueue: { callback: RafCallback; priority: number }[] = [] 6 | 7 | const tick = (t = 0) => { 8 | rafId = requestAnimationFrame(tick) 9 | 10 | let deltaTime = t - now 11 | now = t 12 | 13 | // When leaving the window and switching back after a while 14 | // t gets really big and causes a big jump in the delta time 15 | if (deltaTime > 500) deltaTime = 30 16 | 17 | renderQueue.forEach(({ callback }) => callback(t, deltaTime)) 18 | } 19 | 20 | const startRaf = () => { 21 | now = performance.now() 22 | tick() 23 | } 24 | 25 | const remove = (callback: RafCallback) => { 26 | renderQueue = renderQueue.filter((c) => c.callback !== callback) 27 | if (!renderQueue.length) cancelAnimationFrame(rafId) 28 | } 29 | 30 | export const raf = (callback: RafCallback, priority = 0) => { 31 | if (!renderQueue.length) startRaf() 32 | renderQueue.push({ callback, priority }) 33 | renderQueue.sort((a, b) => a.priority - b.priority) 34 | 35 | return () => remove(callback) 36 | } 37 | -------------------------------------------------------------------------------- /src/random.ts: -------------------------------------------------------------------------------- 1 | export const random = (a = 0, z = 1) => Math.random() * (z - a) + a 2 | 3 | export const randomBool = () => Boolean(random() > 0.5) 4 | 5 | export const randomInt = (a = 0, z = 1) => Math.round(random(a, z)) 6 | -------------------------------------------------------------------------------- /src/randomGaussian.ts: -------------------------------------------------------------------------------- 1 | //https://www.youtube.com/watch?v=hvAFXU9gNcI&ab_channel=ZuluboProductions 2 | // Random number that more likely returns the medium of the standard deviation to more closely mimic a natural randomness 3 | const getRandomGaussian = (standardDeviation = 1) => { 4 | const v1 = Math.random() 5 | const v2 = Math.random() 6 | 7 | const randomStandardNormal = 8 | Math.sqrt(-2 * Math.log10(v1)) * Math.sin(2 * Math.PI * v2) 9 | return randomStandardNormal * standardDeviation 10 | } 11 | 12 | export const randomGaussian = (standardDeviation = 1) => { 13 | const randomNumber = getRandomGaussian(standardDeviation) 14 | 15 | return Math.abs(randomNumber) > standardDeviation 16 | ? randomGaussian(standardDeviation) 17 | : randomNumber 18 | } 19 | -------------------------------------------------------------------------------- /src/react/index.ts: -------------------------------------------------------------------------------- 1 | export { useRaf } from './useRaf' 2 | export { useScroll } from './useScroll' 3 | export { useScrollProgress } from './useScrollProgress' 4 | -------------------------------------------------------------------------------- /src/react/useRaf.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { raf, type RafCallback } from '../raf' 3 | 4 | export const useRaf = (callback: RafCallback, deps = []) => { 5 | useEffect(() => raf(callback), [callback, ...deps]) 6 | } 7 | -------------------------------------------------------------------------------- /src/react/useScroll.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { addScrollCallback, type scrollCallback } from '../scroll' 3 | 4 | export const useScroll = (callback: scrollCallback, deps = []) => { 5 | useEffect(() => addScrollCallback(callback), [...deps, callback]) 6 | } 7 | -------------------------------------------------------------------------------- /src/react/useScrollProgress.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useCallback, useEffect, useRef } from 'react' 2 | import { clamp } from '../clamp' 3 | import { inverseLerp } from '../inverseLerp' 4 | import { type scrollCallback } from '../scroll' 5 | import { useScroll } from './useScroll' 6 | 7 | type ScrollPositionReference = 'top' | 'center' | 'bottom' 8 | 9 | type Options = { 10 | onScroll: scrollCallback 11 | start: ScrollPositionReference // Wrapper top hits either top, bottom or center of viewport 12 | end: ScrollPositionReference // Wrapper bottom hits either top, bottom or center of viewport 13 | } 14 | 15 | const getOffset = (offset: ScrollPositionReference): number => { 16 | const amount = { 17 | top: 0, 18 | center: 0.5, 19 | bottom: 1, 20 | }[offset] 21 | 22 | return window.innerHeight * amount 23 | } 24 | 25 | /* 26 | * Returns scroll progress of an element from 0-1 27 | */ 28 | export const useScrollProgress = ({ 29 | onScroll, 30 | start = 'bottom', 31 | end = 'top', 32 | }: Partial = {}): RefObject => { 33 | const { width: windowWidth, height: windowHeight } = useWindowSize() 34 | 35 | const ref = useRef(null) 36 | const cache = useRef<{ 37 | start: number 38 | end: number 39 | lastProgress: number 40 | }>({ 41 | start: 100, 42 | end: 200, 43 | lastProgress: 0, 44 | }) 45 | 46 | // Calculate scroll start and end values 47 | useEffect(() => { 48 | const wrapper = ref.current 49 | if (!wrapper) return 50 | 51 | const bounds = wrapper.getBoundingClientRect() 52 | const scrollStart = window.scrollY + bounds.top 53 | 54 | cache.current.start = clamp( 55 | scrollStart - getOffset(start), 56 | 0, 57 | Infinity 58 | ) 59 | cache.current.end = scrollStart + bounds.height - getOffset(end) 60 | }, [windowHeight, windowWidth, ref, start, end]) 61 | 62 | const calcScrollProgress = useCallback( 63 | (y: number) => { 64 | if (!cache.current) return 65 | const { start, end, lastProgress } = cache.current 66 | const progress = inverseLerp(start, end, y) 67 | 68 | // Comparing to last progress ensures 0 and 1 progress are always fired 69 | // to properly put the animation on the first or last frame 70 | if (lastProgress <= 1 && lastProgress > 0 && onScroll) { 71 | const clampedProgress = clamp(progress) 72 | 73 | if (onScroll) onScroll(clampedProgress) 74 | } 75 | 76 | cache.current.lastProgress = progress 77 | }, 78 | [cache, onScroll] 79 | ) 80 | 81 | useScroll(calcScrollProgress) 82 | 83 | return ref 84 | } 85 | -------------------------------------------------------------------------------- /src/round.ts: -------------------------------------------------------------------------------- 1 | export const round = (v = 1.001, p = 1000) => Math.round(v * p) / p 2 | -------------------------------------------------------------------------------- /src/scroll.ts: -------------------------------------------------------------------------------- 1 | export type scrollCallback = (progress: number) => void 2 | 3 | let callbacks: scrollCallback[] = [] 4 | 5 | const globalOnScroll = () => { 6 | const y = window.scrollY 7 | callbacks.forEach((cb) => cb(y)) 8 | } 9 | 10 | const start = () => { 11 | window.addEventListener('scroll', globalOnScroll) 12 | } 13 | 14 | const stop = () => { 15 | window.removeEventListener('scroll', globalOnScroll) 16 | } 17 | 18 | export const addScrollCallback = (callback: scrollCallback) => { 19 | if (!callbacks.length) start() 20 | callbacks.push(callback) 21 | 22 | return () => { 23 | callbacks = callbacks.filter((cb) => cb !== callback) 24 | if (!callbacks.length) stop() 25 | } 26 | } 27 | 28 | export const onScroll = (callback: scrollCallback) => { 29 | const removeCallback = addScrollCallback(callback) 30 | 31 | return () => removeCallback() 32 | } 33 | -------------------------------------------------------------------------------- /src/smoothClamp.ts: -------------------------------------------------------------------------------- 1 | import { smoothstep } from './smoothstep' 2 | 3 | export const smoothClamp = (x = 0, min = 0, max = 1) => 4 | smoothstep(min, max, x) * (max - min) + min 5 | -------------------------------------------------------------------------------- /src/smootherstep.ts: -------------------------------------------------------------------------------- 1 | import { clamp } from './clamp' 2 | 3 | // http://en.wikipedia.org/wiki/Smoothstep - Variations 4 | export const smootherstep = (x = 0, min = 0, max = 1) => { 5 | // Scale, and clamp x to 0..1 range 6 | x = clamp((x - min) / (max - min), 0.0, 1.0) 7 | 8 | return x * x * x * (x * (x * 6 - 15) + 10) 9 | } 10 | -------------------------------------------------------------------------------- /src/smoothstep.ts: -------------------------------------------------------------------------------- 1 | import { clamp } from './clamp' 2 | 3 | // http://en.wikipedia.org/wiki/Smoothstep 4 | export const smoothstep = (x = 0, min = 0, max = 1) => { 5 | x = clamp((x - min) / (max - min), 0.0, 1.0) 6 | 7 | return x * x * (3 - 2 * x) 8 | } 9 | -------------------------------------------------------------------------------- /src/spring.ts: -------------------------------------------------------------------------------- 1 | export interface SpringConfig { 2 | stiffness: number 3 | damping: number 4 | mass: number 5 | } 6 | 7 | export interface Spring { 8 | update: () => void 9 | get: () => number 10 | set: (newTarget: number) => void 11 | } 12 | 13 | export interface ArraySpring { 14 | update: Spring['update'] 15 | set: Spring['set'] 16 | get: () => number[] 17 | } 18 | 19 | const createDefaultSpring = ( 20 | start = 0, 21 | { stiffness, damping, mass }: SpringConfig 22 | ): Spring => { 23 | let current = start 24 | let previous = start 25 | let target = start 26 | 27 | return { 28 | update: () => { 29 | const velocity = current - previous 30 | const acceleration = 31 | ((target - current) * stiffness - velocity * damping) / mass 32 | 33 | previous = current 34 | current += velocity + acceleration 35 | }, 36 | get: () => current, 37 | set: (newTarget) => (target = newTarget), 38 | } 39 | } 40 | 41 | const createArraySpring = ( 42 | start = [0], 43 | config: SpringConfig 44 | ): ArraySpring => { 45 | const springs = start.map((v) => createDefaultSpring(v, config)) 46 | 47 | return { 48 | update: () => springs.forEach((spring) => spring.update()), 49 | get: () => springs.map((spring) => spring.get()), 50 | set: (newTarget = 1) => 51 | springs.forEach((spring, i) => spring.set(newTarget[i])), 52 | } 53 | } 54 | 55 | export const createSpring = ( 56 | start = 0, 57 | { stiffness = 0.1, damping = 0.8, mass = 1 } = {} 58 | ): (Spring | ArraySpring) & { config: SpringConfig } => { 59 | const spring = Array.isArray(start) 60 | ? createArraySpring(start, { stiffness, damping, mass }) 61 | : createDefaultSpring(start, { stiffness, damping, mass }) 62 | 63 | return { 64 | ...spring, 65 | config: { stiffness, damping, mass }, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/stateMachine.ts: -------------------------------------------------------------------------------- 1 | export interface State { 2 | name: string 3 | enter: (prevState?: State) => void 4 | exit: () => void 5 | update: (timeElapsed: number) => void 6 | } 7 | 8 | export const state = ({ 9 | name = '', 10 | enter = () => null, 11 | exit = () => null, 12 | update = () => null, 13 | }: Partial): State => ({ name, enter, exit, update }) 14 | 15 | export interface StateMachine { 16 | hasState: (name: State['name']) => boolean 17 | addState: (state: State) => void 18 | setState: (name: State['name']) => void 19 | update: State['update'] 20 | currentState: () => State | null 21 | } 22 | 23 | export const stateMachine = ( 24 | initialStates: State[] = [] 25 | ): StateMachine => { 26 | let currentState: State | null = null 27 | const states: { [name: string]: State } = {} 28 | 29 | const addState = (state: State) => { 30 | if (!state.name) return 31 | states[state.name] = state 32 | } 33 | 34 | initialStates.forEach(addState) 35 | 36 | return { 37 | addState, 38 | hasState: (name) => Boolean(states[name]), 39 | setState: (name: string) => { 40 | const prevState = currentState || undefined 41 | const nextState = states[name] 42 | 43 | if (!nextState) return 44 | 45 | if (prevState && prevState.name !== name) { 46 | prevState.exit() 47 | } 48 | 49 | nextState.enter(prevState) 50 | currentState = nextState 51 | }, 52 | update: (timeElapsed: number) => { 53 | if (currentState) currentState.update(timeElapsed) 54 | }, 55 | currentState: () => currentState, 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/weightedList.ts: -------------------------------------------------------------------------------- 1 | interface WeightedItem { 2 | item: T 3 | weight: number 4 | } 5 | 6 | interface WeightedList { 7 | add(item: T, weight: number): void 8 | remove(item: T): void 9 | get(): T | null 10 | } 11 | 12 | const get = ( 13 | items: WeightedItem[], 14 | totalWeight: number 15 | ): T | null => { 16 | if (!items.length) return null 17 | 18 | const randomWeight = Math.random() * totalWeight 19 | let cumulativeWeight = 0 20 | 21 | for (const weightedItem of items) { 22 | cumulativeWeight += weightedItem.weight 23 | if (randomWeight <= cumulativeWeight) { 24 | return weightedItem.item 25 | } 26 | } 27 | 28 | // When no item was found, return the last item 29 | return items[items.length - 1].item 30 | } 31 | 32 | export const weightedList = ( 33 | initialItems: WeightedItem[] = [] 34 | ): WeightedList => { 35 | const items: WeightedItem[] = [] 36 | let totalWeight: number = 0 37 | 38 | const add = (item: T, weight: number): void => { 39 | if (!weight) return 40 | const _weight = Math.max(0, weight) 41 | 42 | items.push({ item, weight: _weight }) 43 | totalWeight += _weight 44 | } 45 | 46 | const remove = (item: T): void => { 47 | const index = items.findIndex( 48 | (weightedItem) => weightedItem.item === item 49 | ) 50 | 51 | if (index !== -1) { 52 | const removedItem = items.splice(index, 1)[0] 53 | totalWeight -= removedItem.weight 54 | } 55 | } 56 | 57 | // Add initial items to the list 58 | initialItems.forEach(({ item, weight }) => { 59 | add(item, weight) 60 | }) 61 | 62 | return { add, remove, get: () => get(items, totalWeight) } 63 | } 64 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | typescript@^5.4.2: 6 | version "5.4.2" 7 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.2.tgz#0ae9cebcfae970718474fe0da2c090cad6577372" 8 | integrity sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ== 9 | --------------------------------------------------------------------------------