const EasingFunctions = {
// Standard CSS easing functions
ease: t => cubicBezier(0.25, 0.1, 0.25, 1.0)(t),
easeIn: t => cubicBezier(0.42, 0, 1.0, 1.0)(t),
easeOut: t => cubicBezier(0, 0, 0.58, 1.0)(t),
easeInOut: t => cubicBezier(0.42, 0, 0.58, 1.0)(t),
easeOutQuad: t => t * (2 - t),
easeInCubic: t => t * t * t,
easeOutCubic: t => (--t) * t * t + 1,
easeInOutCubic: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
easeInQuart: t => t * t * t * t,
easeOutQuart: t => 1 - (--t) * t * t * t,
easeInOutQuart: t => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t,
easeInQuint: t => t * t * t * t * t,
easeOutQuint: t => 1 + (--t) * t * t * t * t,
easeInOutQuint: t => t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t,
easeInSine: t => 1 - Math.cos(t * Math.PI / 2),
easeOutSine: t => Math.sin(t * Math.PI / 2),
easeInOutSine: t => -(Math.cos(Math.PI * t) - 1) / 2,
easeInExpo: t => t === 0 ? 0 : Math.pow(2, 10 * t - 10),
easeOutExpo: t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t),
easeInOutExpo: t => t === 0 ? 0 : t === 1 ? 1 : t < 0.5 ?
Math.pow(2, 20 * t - 10) / 2 : (2 - Math.pow(2, -20 * t + 10)) / 2,
easeInCirc: t => 1 - Math.sqrt(1 - t * t),
easeOutCirc: t => Math.sqrt(1 - (--t) * t),
easeInOutCirc: t => t < 0.5 ?
(1 - Math.sqrt(1 - 4 * t * t)) / 2 : (Math.sqrt(1 - 4 * (t - 1) * t) + 1) / 2
// Helper function for cubic-bezier
function cubicBezier(x1, y1, x2, y2) {
if (t === 0 || t === 1) return t;
const bx = 3 * (x2 - x1) - cx;
const by = 3 * (y2 - y1) - cy;
const sampleCurveX = t => ((ax * t + bx) * t + cx) * t;
const sampleCurveY = t => ((ay * t + by) * t + cy) * t;
const sampleCurveDerivativeX = t => (3 * ax * t + 2 * bx) * t + cx;
for (let i = 0; i < 8; i++) {
const currentX = sampleCurveX(x) - t;
if (Math.abs(currentX) < 1e-7) break;
const derivative = sampleCurveDerivativeX(x);
if (Math.abs(derivative) < 1e-7) break;
x -= currentX / derivative;
function getScrollPosition(target) {
if (typeof target === 'number') {
if (typeof target === 'string') {
const element = document.querySelector(target);
console.warn(`Element with selector "${target}" not found`);
return element.getBoundingClientRect().top + window.pageYOffset;
if (target instanceof Element) {
return target.getBoundingClientRect().top + window.pageYOffset;
console.warn('Invalid target specified');
function scrollToPosition({
easingFunction = EasingFunctions.linear,
const targetPosition = getScrollPosition(target) - offset;
const startingY = window.pageYOffset;
const diff = targetPosition - startingY;
function step(timestamp) {
if (!start) start = timestamp;
const time = timestamp - start;
const percent = Math.min(time / duration, 1);
const eased = easingFunction(percent);
top: startingY + diff * eased,
behavior: 'auto' // Prevent conflict with smooth scrolling
animationFrame = window.requestAnimationFrame(step);
// If duration is 0, immediately scroll to position
animationFrame = window.requestAnimationFrame(step);
return function cleanup() {
window.cancelAnimationFrame(animationFrame);
// 1. Scroll to specific position
easingFunction: EasingFunctions.ease
// 2. Scroll to element by selector
// // target: '#my-element',
// target: document.querySelectorAll('h2')[10],
// offset: 20, // Add 20px offset from the top
// easingFunction: EasingFunctions.easeInOutQuad,
// onComplete: () => console.log('Scrolled to element!')
// 3. Scroll to element reference
// const element = document.querySelector('.some-element');
// easingFunction: EasingFunctions.easeInOutCubic