Skip to content

Instantly share code, notes, and snippets.

@maximilliangeorge
Last active January 8, 2026 16:16
Show Gist options
  • Select an option

  • Save maximilliangeorge/f5f1e9840942be5af2f09f29b7cb3a25 to your computer and use it in GitHub Desktop.

Select an option

Save maximilliangeorge/f5f1e9840942be5af2f09f29b7cb3a25 to your computer and use it in GitHub Desktop.
Utility to scroll through a website for demonstration purposes
// Cubic Bezier utility (from CSS spec)
function cubicBezier(p1x, p1y, p2x, p2y) {
const cx = 3 * p1x
const bx = 3 * (p2x - p1x) - cx
const ax = 1 - cx - bx
const cy = 3 * p1y
const by = 3 * (p2y - p1y) - cy
const ay = 1 - cy - by
function sampleX(t) {
return ((ax * t + bx) * t + cx) * t
}
function sampleY(t) {
return ((ay * t + by) * t + cy) * t
}
function sampleDX(t) {
return (3 * ax * t + 2 * bx) * t + cx
}
// Given x, find t via Newton–Raphson
function solveT(x) {
let t = x
for (let i = 0; i < 5; i++) {
const dx = sampleX(t) - x
const d = sampleDX(t)
if (Math.abs(dx) < 1e-6 || d === 0) break
t -= dx / d
}
return t
}
return function (x) {
if (x <= 0) return 0
if (x >= 1) return 1
return sampleY(solveT(x))
}
}
// Named easings (CSS-compatible)
const easings = {
linear: cubicBezier(0, 0, 1, 1),
'ease': cubicBezier(0.25, 0.1, 0.25, 1),
'ease-in': cubicBezier(0.42, 0, 1, 1),
'ease-out': cubicBezier(0, 0, 0.58, 1),
'ease-in-out': cubicBezier(0.42, 0, 0.58, 1),
'cubic-out': cubicBezier(0.33, 1, 0.68, 1),
'cubic-in': cubicBezier(0.32, 0, 0.67, 0),
'cubic-in-out': cubicBezier(0.65, 0, 0.35, 1),
'scroll-1': cubicBezier(0.43, 0.19, 0, 1)
}
function scrollToY(targetY, duration = 600, easing = 'ease-in-out') {
return new Promise(resolve => {
const startY = window.scrollY
const delta = targetY - startY
const startTime = performance.now()
const ease = easings[easing] ?? easings.linear
let lastY = startY
function tick(now) {
const t = Math.min((now - startTime) / duration, 1)
const desiredY = startY + delta * ease(t)
const dy = desiredY - lastY
// Incremental scroll → preserves momentum
window.scrollBy(0, dy)
lastY += dy
if (t < 1) {
requestAnimationFrame(tick)
} else {
resolve()
}
}
requestAnimationFrame(tick)
})
}
function pause(ms) {
return new Promise(resolve => {
const start = performance.now()
function tick(now) {
// 0.1px drift keeps scroll state alive
window.scrollBy(0, 0.1)
if (now - start < ms) {
requestAnimationFrame(tick)
} else {
resolve()
}
}
requestAnimationFrame(tick)
})
}
function runOnFirstGesture(fn) {
let started = false
function start() {
if (started) return
started = true
window.removeEventListener('touchstart', start)
window.removeEventListener('wheel', start)
fn()
}
window.addEventListener('touchstart', start, { passive: true })
window.addEventListener('wheel', start, { passive: true })
}
async function scrollPipeMomentum(steps) {
for (const [targetY, duration, easing] of steps) {
if (targetY == null) {
await pause(duration)
} else {
await scrollToY(targetY, duration, easing)
}
}
}
async function scrollPipe(steps) {
runOnFirstGesture(() => {
scrollPipeMomentum(steps)
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment