lenis
Version:
How smooth scroll should be
1,043 lines (1,042 loc) • 32.7 kB
JavaScript
//#region package.json
var version = "1.3.25";
//#endregion
//#region packages/core/src/maths.ts
/**
* Clamp a value between a minimum and maximum value
*
* @param min Minimum value
* @param input Value to clamp
* @param max Maximum value
* @returns Clamped value
*/
function clamp(min, input, max) {
return Math.max(min, Math.min(input, max));
}
/**
* Linearly interpolate between two values using an amount (0 <= t <= 1)
*
* @param x First value
* @param y Second value
* @param t Amount to interpolate (0 <= t <= 1)
* @returns Interpolated value
*/
function lerp(x, y, t) {
return (1 - t) * x + t * y;
}
/**
* Damp a value over time using a damping factor
* {@link http://www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/}
*
* @param x Initial value
* @param y Target value
* @param lambda Damping factor
* @param dt Time elapsed since the last update
* @returns Damped value
*/
function damp(x, y, lambda, deltaTime) {
return lerp(x, y, 1 - Math.exp(-lambda * deltaTime));
}
/**
* Calculate the modulo of the dividend and divisor while keeping the result within the same sign as the divisor
* {@link https://anguscroll.com/just/just-modulo}
*
* @param n Dividend
* @param d Divisor
* @returns Modulo
*/
function modulo(n, d) {
return (n % d + d) % d;
}
//#endregion
//#region packages/core/src/animate.ts
/**
* Animate class to handle value animations with lerping or easing
*
* @example
* const animate = new Animate()
* animate.fromTo(0, 100, { duration: 1, easing: (t) => t })
* animate.advance(0.5) // 50
*/
var Animate = class {
isRunning = false;
value = 0;
from = 0;
to = 0;
currentTime = 0;
lerp;
duration;
easing;
onUpdate;
/**
* Advance the animation by the given delta time
*
* @param deltaTime - The time in seconds to advance the animation
*/
advance(deltaTime) {
if (!this.isRunning) return;
let completed = false;
if (this.duration && this.easing) {
this.currentTime += deltaTime;
const linearProgress = clamp(0, this.currentTime / this.duration, 1);
completed = linearProgress >= 1;
const easedProgress = completed ? 1 : this.easing(linearProgress);
this.value = this.from + (this.to - this.from) * easedProgress;
} else if (this.lerp) {
this.value = damp(this.value, this.to, this.lerp * 60, deltaTime);
if (Math.round(this.value) === Math.round(this.to)) {
this.value = this.to;
completed = true;
}
} else {
this.value = this.to;
completed = true;
}
if (completed) this.stop();
this.onUpdate?.(this.value, completed);
}
/** Stop the animation */
stop() {
this.isRunning = false;
}
/**
* Set up the animation from a starting value to an ending value
* with optional parameters for lerping, duration, easing, and onUpdate callback
*
* @param from - The starting value
* @param to - The ending value
* @param options - Options for the animation
*/
fromTo(from, to, { lerp, duration, easing, onStart, onUpdate }) {
this.from = this.value = from;
this.to = to;
this.lerp = lerp;
this.duration = duration;
this.easing = easing;
this.currentTime = 0;
this.isRunning = true;
onStart?.();
this.onUpdate = onUpdate;
}
};
//#endregion
//#region packages/core/src/debounce.ts
function debounce(callback, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
timer = void 0;
callback.apply(this, args);
}, delay);
};
}
//#endregion
//#region packages/core/src/dimensions.ts
/**
* Dimensions class to handle the size of the content and wrapper
*
* @example
* const dimensions = new Dimensions(wrapper, content)
* dimensions.on('resize', (e) => {
* console.log(e.width, e.height)
* })
*/
var Dimensions = class {
width = 0;
height = 0;
scrollHeight = 0;
scrollWidth = 0;
debouncedResize;
wrapperResizeObserver;
contentResizeObserver;
constructor(wrapper, content, { autoResize = true, debounce: debounceValue = 250 } = {}) {
this.wrapper = wrapper;
this.content = content;
if (autoResize) {
this.debouncedResize = debounce(this.resize, debounceValue);
if (this.wrapper instanceof Window) window.addEventListener("resize", this.debouncedResize);
else {
this.wrapperResizeObserver = new ResizeObserver(this.debouncedResize);
this.wrapperResizeObserver.observe(this.wrapper);
}
this.contentResizeObserver = new ResizeObserver(this.debouncedResize);
this.contentResizeObserver.observe(this.content);
}
this.resize();
}
destroy() {
this.wrapperResizeObserver?.disconnect();
this.contentResizeObserver?.disconnect();
if (this.wrapper === window && this.debouncedResize) window.removeEventListener("resize", this.debouncedResize);
}
resize = () => {
this.onWrapperResize();
this.onContentResize();
};
onWrapperResize = () => {
if (this.wrapper instanceof Window) {
this.width = window.innerWidth;
this.height = window.innerHeight;
} else {
this.width = this.wrapper.clientWidth;
this.height = this.wrapper.clientHeight;
}
};
onContentResize = () => {
if (this.wrapper instanceof Window) {
this.scrollHeight = this.content.scrollHeight;
this.scrollWidth = this.content.scrollWidth;
} else {
this.scrollHeight = this.wrapper.scrollHeight;
this.scrollWidth = this.wrapper.scrollWidth;
}
};
get limit() {
return {
x: this.scrollWidth - this.width,
y: this.scrollHeight - this.height
};
}
};
//#endregion
//#region packages/core/src/emitter.ts
/**
* Emitter class to handle events
* @example
* const emitter = new Emitter()
* emitter.on('event', (data) => {
* console.log(data)
* })
* emitter.emit('event', 'data')
*/
var Emitter = class {
events = {};
/**
* Emit an event with the given data
* @param event Event name
* @param args Data to pass to the event handlers
*/
emit(event, ...args) {
const callbacks = this.events[event] || [];
for (let i = 0, length = callbacks.length; i < length; i++) callbacks[i]?.(...args);
}
/**
* Add a callback to the event
* @param event Event name
* @param cb Callback function
* @returns Unsubscribe function
*/
on(event, cb) {
if (this.events[event]) this.events[event].push(cb);
else this.events[event] = [cb];
return () => {
this.events[event] = this.events[event]?.filter((i) => cb !== i);
};
}
/**
* Remove a callback from the event
* @param event Event name
* @param callback Callback function
*/
off(event, callback) {
this.events[event] = this.events[event]?.filter((i) => callback !== i);
}
/**
* Remove all event listeners and clean up
*/
destroy() {
this.events = {};
}
};
//#endregion
//#region packages/core/src/virtual-scroll.ts
const LINE_HEIGHT = 100 / 6;
const listenerOptions = { passive: false };
function getDeltaMultiplier(deltaMode, size) {
if (deltaMode === 1) return LINE_HEIGHT;
if (deltaMode === 2) return size;
return 1;
}
var VirtualScroll = class {
touchStart = {
x: 0,
y: 0
};
lastDelta = {
x: 0,
y: 0
};
window = {
width: 0,
height: 0
};
emitter = new Emitter();
constructor(element, options = {
wheelMultiplier: 1,
touchMultiplier: 1
}) {
this.element = element;
this.options = options;
window.addEventListener("resize", this.onWindowResize);
this.onWindowResize();
this.element.addEventListener("wheel", this.onWheel, listenerOptions);
this.element.addEventListener("touchstart", this.onTouchStart, listenerOptions);
this.element.addEventListener("touchmove", this.onTouchMove, listenerOptions);
this.element.addEventListener("touchend", this.onTouchEnd, listenerOptions);
}
/**
* Add an event listener for the given event and callback
*
* @param event Event name
* @param callback Callback function
*/
on(event, callback) {
return this.emitter.on(event, callback);
}
/** Remove all event listeners and clean up */
destroy() {
this.emitter.destroy();
window.removeEventListener("resize", this.onWindowResize);
this.element.removeEventListener("wheel", this.onWheel, listenerOptions);
this.element.removeEventListener("touchstart", this.onTouchStart, listenerOptions);
this.element.removeEventListener("touchmove", this.onTouchMove, listenerOptions);
this.element.removeEventListener("touchend", this.onTouchEnd, listenerOptions);
}
/**
* Event handler for 'touchstart' event
*
* @param event Touch event
*/
onTouchStart = (event) => {
const { clientX, clientY } = event.targetTouches ? event.targetTouches[0] : event;
this.touchStart.x = clientX;
this.touchStart.y = clientY;
this.lastDelta = {
x: 0,
y: 0
};
this.emitter.emit("scroll", {
deltaX: 0,
deltaY: 0,
event
});
};
/** Event handler for 'touchmove' event */
onTouchMove = (event) => {
const { clientX, clientY } = event.targetTouches ? event.targetTouches[0] : event;
const deltaX = -(clientX - this.touchStart.x) * this.options.touchMultiplier;
const deltaY = -(clientY - this.touchStart.y) * this.options.touchMultiplier;
this.touchStart.x = clientX;
this.touchStart.y = clientY;
this.lastDelta = {
x: deltaX,
y: deltaY
};
this.emitter.emit("scroll", {
deltaX,
deltaY,
event
});
};
onTouchEnd = (event) => {
this.emitter.emit("scroll", {
deltaX: this.lastDelta.x,
deltaY: this.lastDelta.y,
event
});
};
/** Event handler for 'wheel' event */
onWheel = (event) => {
let { deltaX, deltaY, deltaMode } = event;
const multiplierX = getDeltaMultiplier(deltaMode, this.window.width);
const multiplierY = getDeltaMultiplier(deltaMode, this.window.height);
deltaX *= multiplierX;
deltaY *= multiplierY;
deltaX *= this.options.wheelMultiplier;
deltaY *= this.options.wheelMultiplier;
this.emitter.emit("scroll", {
deltaX,
deltaY,
event
});
};
onWindowResize = () => {
this.window = {
width: window.innerWidth,
height: window.innerHeight
};
};
};
//#endregion
//#region packages/core/src/lenis.ts
const defaultEasing = (t) => Math.min(1, 1.001 - 2 ** (-10 * t));
var Lenis = class {
_isScrolling = false;
_isStopped = false;
_isLocked = false;
_preventNextNativeScrollEvent = false;
_resetVelocityTimeout = null;
_rafId = null;
_isDraggingSelection = false;
/**
* Whether or not the user is touching the screen
*/
isTouching;
/**
* Whether or not the device is running iOS
*/
isIos;
/**
* The time in ms since the lenis instance was created
*/
time = 0;
/**
* User data that will be forwarded through the scroll event
*
* @example
* lenis.scrollTo(100, {
* userData: {
* foo: 'bar'
* }
* })
*/
userData = {};
/**
* The last velocity of the scroll
*/
lastVelocity = 0;
/**
* The current velocity of the scroll
*/
velocity = 0;
/**
* The direction of the scroll
*/
direction = 0;
/**
* The options passed to the lenis instance
*/
options;
/**
* The target scroll value
*/
targetScroll;
/**
* The animated scroll value
*/
animatedScroll;
animate = new Animate();
emitter = new Emitter();
dimensions;
virtualScroll;
constructor({ wrapper = window, content = document.documentElement, eventsTarget = wrapper, smoothWheel = true, syncTouch = false, syncTouchLerp = .075, touchInertiaExponent = 1.7, duration, easing, lerp = .1, infinite = false, orientation = "vertical", gestureOrientation = orientation === "horizontal" ? "both" : "vertical", touchMultiplier = 1, wheelMultiplier = 1, autoResize = true, prevent, virtualScroll, overscroll = true, autoRaf = false, anchors = false, autoToggle = false, allowNestedScroll = false, __experimental__naiveDimensions = false, naiveDimensions = __experimental__naiveDimensions, stopInertiaOnNavigate = false } = {}) {
window.lenisVersion = version;
if (!window.lenis) window.lenis = {};
window.lenis.version = version;
if (orientation === "horizontal") window.lenis.horizontal = true;
if (syncTouch === true) window.lenis.touch = true;
this.isIos = /(iPad|iPhone|iPod)/g.test(navigator.userAgent);
if (!wrapper || wrapper === document.documentElement) wrapper = window;
if (typeof duration === "number" && typeof easing !== "function") easing = defaultEasing;
else if (typeof easing === "function" && typeof duration !== "number") duration = 1;
this.options = {
wrapper,
content,
eventsTarget,
smoothWheel,
syncTouch,
syncTouchLerp,
touchInertiaExponent,
duration,
easing,
lerp,
infinite,
gestureOrientation,
orientation,
touchMultiplier,
wheelMultiplier,
autoResize,
prevent,
virtualScroll,
overscroll,
autoRaf,
anchors,
autoToggle,
allowNestedScroll,
naiveDimensions,
stopInertiaOnNavigate
};
this.dimensions = new Dimensions(wrapper, content, { autoResize });
this.updateClassName();
this.targetScroll = this.animatedScroll = this.actualScroll;
this.options.wrapper.addEventListener("scroll", this.onNativeScroll);
this.options.wrapper.addEventListener("scrollend", this.onScrollEnd, { capture: true });
if (this.options.anchors || this.options.stopInertiaOnNavigate) this.options.wrapper.addEventListener("click", this.onClick);
this.options.wrapper.addEventListener("pointerdown", this.onPointerDown);
this.virtualScroll = new VirtualScroll(eventsTarget, {
touchMultiplier,
wheelMultiplier
});
this.virtualScroll.on("scroll", this.onVirtualScroll);
if (this.options.autoToggle) {
this.checkOverflow();
this.rootElement.addEventListener("transitionend", this.onTransitionEnd);
}
if (this.options.autoRaf) this._rafId = requestAnimationFrame(this.raf);
}
/**
* Destroy the lenis instance, remove all event listeners and clean up the class name
*/
destroy() {
this.emitter.destroy();
this.options.wrapper.removeEventListener("scroll", this.onNativeScroll);
this.options.wrapper.removeEventListener("scrollend", this.onScrollEnd, { capture: true });
this.options.wrapper.removeEventListener("pointerdown", this.onPointerDown);
if (this.options.anchors || this.options.stopInertiaOnNavigate) this.options.wrapper.removeEventListener("click", this.onClick);
this.virtualScroll.destroy();
this.dimensions.destroy();
this.cleanUpClassName();
if (this._rafId) cancelAnimationFrame(this._rafId);
}
on(event, callback) {
return this.emitter.on(event, callback);
}
off(event, callback) {
return this.emitter.off(event, callback);
}
onScrollEnd = (e) => {
if (!(e instanceof CustomEvent)) {
if (this.isScrolling === "smooth" || this.isScrolling === false) e.stopPropagation();
}
};
dispatchScrollendEvent = () => {
this.options.wrapper.dispatchEvent(new CustomEvent("scrollend", {
bubbles: this.options.wrapper === window,
detail: { lenisScrollEnd: true }
}));
};
get overflow() {
const property = this.isHorizontal ? "overflow-x" : "overflow-y";
return getComputedStyle(this.rootElement)[property];
}
checkOverflow() {
if (["hidden", "clip"].includes(this.overflow)) this.internalStop();
else this.internalStart();
}
onTransitionEnd = (event) => {
if (event.propertyName?.includes("overflow") && event.target === this.rootElement) this.checkOverflow();
};
setScroll(scroll) {
if (this.isHorizontal) this.options.wrapper.scrollTo({
left: scroll,
behavior: "instant"
});
else this.options.wrapper.scrollTo({
top: scroll,
behavior: "instant"
});
}
onClick = (event) => {
const linkElementsUrls = event.composedPath().filter((node) => node instanceof HTMLAnchorElement && node.href).map((element) => new URL(element.href));
const currentUrl = new URL(window.location.href);
if (this.options.anchors) {
const anchorElementUrl = linkElementsUrls.find((targetUrl) => currentUrl.host === targetUrl.host && currentUrl.pathname === targetUrl.pathname && targetUrl.hash);
if (anchorElementUrl) {
const options = typeof this.options.anchors === "object" && this.options.anchors ? this.options.anchors : void 0;
const target = decodeURIComponent(anchorElementUrl.hash);
this.scrollTo(target, options);
return;
}
}
if (this.options.stopInertiaOnNavigate) {
if (linkElementsUrls.some((targetUrl) => currentUrl.host === targetUrl.host && currentUrl.pathname !== targetUrl.pathname)) {
this.reset();
return;
}
}
};
onPointerDown = (event) => {
if (event.button === 1) this.reset();
};
isTouchOnSelectionHandle(event) {
const selection = window.getSelection();
if (!selection || selection.isCollapsed || selection.rangeCount === 0) return false;
const touch = event.targetTouches[0] ?? event.changedTouches[0];
if (!touch) return false;
const rects = selection.getRangeAt(0).getClientRects();
if (rects.length === 0) return false;
const first = rects[0];
const last = rects[rects.length - 1];
const HANDLE_RADIUS = 40;
const nearStart = Math.hypot(touch.clientX - first.left, touch.clientY - first.top) <= HANDLE_RADIUS;
const nearEnd = Math.hypot(touch.clientX - last.right, touch.clientY - last.bottom) <= HANDLE_RADIUS;
return nearStart || nearEnd;
}
onVirtualScroll = (data) => {
if (typeof this.options.virtualScroll === "function" && this.options.virtualScroll(data) === false) return;
const { deltaX, deltaY, event } = data;
this.emitter.emit("virtual-scroll", {
deltaX,
deltaY,
event
});
if (event.ctrlKey) return;
if (event.lenisStopPropagation) return;
const isTouch = event.type.includes("touch");
const isWheel = event.type.includes("wheel");
if (isTouch && this.isIos) {
if (event.type === "touchstart") this._isDraggingSelection = this.isTouchOnSelectionHandle(event);
if (this._isDraggingSelection) {
if (event.type === "touchend") this._isDraggingSelection = false;
return;
}
}
this.isTouching = event.type === "touchstart" || event.type === "touchmove";
const isClickOrTap = deltaX === 0 && deltaY === 0;
if (this.options.syncTouch && isTouch && event.type === "touchstart" && isClickOrTap && !this.isStopped && !this.isLocked) {
this.reset();
return;
}
const isUnknownGesture = this.options.gestureOrientation === "vertical" && deltaY === 0 || this.options.gestureOrientation === "horizontal" && deltaX === 0;
if (isClickOrTap || isUnknownGesture) return;
let composedPath = event.composedPath();
composedPath = composedPath.slice(0, composedPath.indexOf(this.rootElement));
const prevent = this.options.prevent;
const gestureOrientation = Math.abs(deltaX) >= Math.abs(deltaY) ? "horizontal" : "vertical";
if (composedPath.find((node) => node instanceof HTMLElement && (typeof prevent === "function" && prevent?.(node) || node.hasAttribute?.("data-lenis-prevent") || gestureOrientation === "vertical" && node.hasAttribute?.("data-lenis-prevent-vertical") || gestureOrientation === "horizontal" && node.hasAttribute?.("data-lenis-prevent-horizontal") || isTouch && node.hasAttribute?.("data-lenis-prevent-touch") || isWheel && node.hasAttribute?.("data-lenis-prevent-wheel") || this.options.allowNestedScroll && this.hasNestedScroll(node, {
deltaX,
deltaY
})))) return;
if (this.isStopped || this.isLocked) {
if (event.cancelable) event.preventDefault();
return;
}
if (!(this.options.syncTouch && isTouch || this.options.smoothWheel && isWheel)) {
this.isScrolling = "native";
this.animate.stop();
event.lenisStopPropagation = true;
return;
}
let delta = deltaY;
if (this.options.gestureOrientation === "both") delta = Math.abs(deltaY) > Math.abs(deltaX) ? deltaY : deltaX;
else if (this.options.gestureOrientation === "horizontal") delta = deltaX;
if (!this.options.overscroll || this.options.infinite || this.options.wrapper !== window && this.limit > 0 && (this.animatedScroll > 0 && this.animatedScroll < this.limit || this.animatedScroll === 0 && deltaY > 0 || this.animatedScroll === this.limit && deltaY < 0)) event.lenisStopPropagation = true;
if (event.cancelable) event.preventDefault();
const isSyncTouch = isTouch && this.options.syncTouch;
const hasTouchInertia = isTouch && event.type === "touchend";
if (hasTouchInertia) delta = Math.sign(delta) * Math.abs(this.velocity) ** this.options.touchInertiaExponent;
this.scrollTo(this.targetScroll + delta, {
programmatic: false,
...isSyncTouch ? { lerp: hasTouchInertia ? this.options.syncTouchLerp : 1 } : {
lerp: this.options.lerp,
duration: this.options.duration,
easing: this.options.easing
}
});
};
/**
* Force lenis to recalculate the dimensions
*/
resize() {
this.dimensions.resize();
this.animatedScroll = this.targetScroll = this.actualScroll;
this.emit();
}
emit() {
this.emitter.emit("scroll", this);
}
onNativeScroll = () => {
if (this._resetVelocityTimeout !== null) {
clearTimeout(this._resetVelocityTimeout);
this._resetVelocityTimeout = null;
}
if (this._preventNextNativeScrollEvent) {
this._preventNextNativeScrollEvent = false;
return;
}
if (this.isScrolling === false || this.isScrolling === "native") {
const lastScroll = this.animatedScroll;
this.animatedScroll = this.targetScroll = this.actualScroll;
this.lastVelocity = this.velocity;
this.velocity = this.animatedScroll - lastScroll;
this.direction = Math.sign(this.animatedScroll - lastScroll);
if (!this.isStopped) this.isScrolling = "native";
this.emit();
if (this.velocity !== 0) this._resetVelocityTimeout = setTimeout(() => {
this.lastVelocity = this.velocity;
this.velocity = 0;
this.isScrolling = false;
this.emit();
}, 400);
}
};
reset() {
this.isLocked = false;
this.isScrolling = false;
this.animatedScroll = this.targetScroll = this.actualScroll;
this.lastVelocity = this.velocity = 0;
this.animate.stop();
}
/**
* Start lenis scroll after it has been stopped
*/
start() {
if (!this.isStopped) return;
if (this.options.autoToggle) {
this.rootElement.style.removeProperty("overflow");
return;
}
this.internalStart();
}
internalStart() {
if (!this.isStopped) return;
this.reset();
this.isStopped = false;
this.emit();
}
/**
* Stop lenis scroll
*/
stop() {
if (this.isStopped) return;
if (this.options.autoToggle) {
this.rootElement.style.setProperty("overflow", "clip");
return;
}
this.internalStop();
}
internalStop() {
if (this.isStopped) return;
this.reset();
this.isStopped = true;
this.emit();
}
/**
* RequestAnimationFrame for lenis
*
* @param time The time in ms from an external clock like `requestAnimationFrame` or Tempus
*/
raf = (time) => {
const deltaTime = time - (this.time || time);
this.time = time;
this.animate.advance(deltaTime * .001);
if (this.options.autoRaf) this._rafId = requestAnimationFrame(this.raf);
};
/**
* Scroll to a target value
*
* @param target The target value to scroll to
* @param options The options for the scroll
*
* @example
* lenis.scrollTo(100, {
* offset: 100,
* duration: 1,
* easing: (t) => 1 - Math.cos((t * Math.PI) / 2),
* lerp: 0.1,
* onStart: () => {
* console.log('onStart')
* },
* onComplete: () => {
* console.log('onComplete')
* },
* })
*/
scrollTo(_target, { offset = 0, immediate = false, lock = false, programmatic = true, lerp = programmatic ? this.options.lerp : void 0, duration = programmatic ? this.options.duration : void 0, easing = programmatic ? this.options.easing : void 0, onStart, onComplete, force = false, userData } = {}) {
if ((this.isStopped || this.isLocked) && !force) return;
let target = _target;
let adjustedOffset = offset;
if (typeof target === "string" && [
"top",
"left",
"start",
"#"
].includes(target)) target = 0;
else if (typeof target === "string" && [
"bottom",
"right",
"end"
].includes(target)) target = this.limit;
else {
let node = null;
if (typeof target === "string") {
node = target.startsWith("#") ? document.getElementById(target.slice(1)) : document.querySelector(target);
if (!node) if (target === "#top") target = 0;
else console.warn("Lenis: Target not found", target);
} else if (target instanceof HTMLElement && target?.nodeType) node = target;
if (node) {
if (this.options.wrapper !== window) {
const wrapperRect = this.rootElement.getBoundingClientRect();
adjustedOffset -= this.isHorizontal ? wrapperRect.left : wrapperRect.top;
}
const rect = node.getBoundingClientRect();
const targetStyle = getComputedStyle(node);
const scrollMargin = this.isHorizontal ? Number.parseFloat(targetStyle.scrollMarginLeft) : Number.parseFloat(targetStyle.scrollMarginTop);
const containerStyle = getComputedStyle(this.rootElement);
const scrollPadding = this.isHorizontal ? Number.parseFloat(containerStyle.scrollPaddingLeft) : Number.parseFloat(containerStyle.scrollPaddingTop);
target = (this.isHorizontal ? rect.left : rect.top) + this.animatedScroll - (Number.isNaN(scrollMargin) ? 0 : scrollMargin) - (Number.isNaN(scrollPadding) ? 0 : scrollPadding);
}
}
if (typeof target !== "number") return;
target += adjustedOffset;
if (this.options.infinite) {
if (programmatic) {
this.targetScroll = this.animatedScroll = this.scroll;
const distance = target - this.animatedScroll;
if (distance > this.limit / 2) target -= this.limit;
else if (distance < -this.limit / 2) target += this.limit;
}
} else target = clamp(0, target, this.limit);
if (target === this.targetScroll) {
onStart?.(this);
onComplete?.(this);
return;
}
this.userData = userData ?? {};
if (immediate) {
this.animatedScroll = this.targetScroll = target;
this.setScroll(this.scroll);
this.reset();
this.preventNextNativeScrollEvent();
this.emit();
onComplete?.(this);
this.userData = {};
requestAnimationFrame(() => {
this.dispatchScrollendEvent();
});
return;
}
if (!programmatic) this.targetScroll = target;
if (typeof duration === "number" && typeof easing !== "function") easing = defaultEasing;
else if (typeof easing === "function" && typeof duration !== "number") duration = 1;
this.animate.fromTo(this.animatedScroll, target, {
duration,
easing,
lerp,
onStart: () => {
if (lock) this.isLocked = true;
this.isScrolling = "smooth";
onStart?.(this);
},
onUpdate: (value, completed) => {
this.isScrolling = "smooth";
this.lastVelocity = this.velocity;
this.velocity = value - this.animatedScroll;
this.direction = Math.sign(this.velocity);
this.animatedScroll = value;
this.setScroll(this.scroll);
if (programmatic) this.targetScroll = value;
if (!completed) this.emit();
if (completed) {
this.reset();
this.emit();
onComplete?.(this);
this.userData = {};
requestAnimationFrame(() => {
this.dispatchScrollendEvent();
});
this.preventNextNativeScrollEvent();
}
}
});
}
preventNextNativeScrollEvent() {
this._preventNextNativeScrollEvent = true;
requestAnimationFrame(() => {
this._preventNextNativeScrollEvent = false;
});
}
hasNestedScroll(node, { deltaX, deltaY }) {
const time = Date.now();
if (!node._lenis) node._lenis = {};
const cache = node._lenis;
let hasOverflowX;
let hasOverflowY;
let isScrollableX;
let isScrollableY;
let hasOverscrollBehaviorX;
let hasOverscrollBehaviorY;
let scrollWidth;
let scrollHeight;
let clientWidth;
let clientHeight;
if (time - (cache.time ?? 0) > 2e3) {
cache.time = Date.now();
const computedStyle = window.getComputedStyle(node);
cache.computedStyle = computedStyle;
hasOverflowX = [
"auto",
"overlay",
"scroll"
].includes(computedStyle.overflowX);
hasOverflowY = [
"auto",
"overlay",
"scroll"
].includes(computedStyle.overflowY);
hasOverscrollBehaviorX = ["auto"].includes(computedStyle.overscrollBehaviorX);
hasOverscrollBehaviorY = ["auto"].includes(computedStyle.overscrollBehaviorY);
cache.hasOverflowX = hasOverflowX;
cache.hasOverflowY = hasOverflowY;
if (!(hasOverflowX || hasOverflowY)) return false;
scrollWidth = node.scrollWidth;
scrollHeight = node.scrollHeight;
clientWidth = node.clientWidth;
clientHeight = node.clientHeight;
isScrollableX = scrollWidth > clientWidth;
isScrollableY = scrollHeight > clientHeight;
cache.isScrollableX = isScrollableX;
cache.isScrollableY = isScrollableY;
cache.scrollWidth = scrollWidth;
cache.scrollHeight = scrollHeight;
cache.clientWidth = clientWidth;
cache.clientHeight = clientHeight;
cache.hasOverscrollBehaviorX = hasOverscrollBehaviorX;
cache.hasOverscrollBehaviorY = hasOverscrollBehaviorY;
} else {
isScrollableX = cache.isScrollableX;
isScrollableY = cache.isScrollableY;
hasOverflowX = cache.hasOverflowX;
hasOverflowY = cache.hasOverflowY;
scrollWidth = cache.scrollWidth;
scrollHeight = cache.scrollHeight;
clientWidth = cache.clientWidth;
clientHeight = cache.clientHeight;
hasOverscrollBehaviorX = cache.hasOverscrollBehaviorX;
hasOverscrollBehaviorY = cache.hasOverscrollBehaviorY;
}
if (!(hasOverflowX && isScrollableX || hasOverflowY && isScrollableY)) return false;
const orientation = Math.abs(deltaX) >= Math.abs(deltaY) ? "horizontal" : "vertical";
let scroll;
let maxScroll;
let delta;
let hasOverflow;
let isScrollable;
let hasOverscrollBehavior;
if (orientation === "horizontal") {
scroll = Math.round(node.scrollLeft);
maxScroll = scrollWidth - clientWidth;
delta = deltaX;
hasOverflow = hasOverflowX;
isScrollable = isScrollableX;
hasOverscrollBehavior = hasOverscrollBehaviorX;
} else if (orientation === "vertical") {
scroll = Math.round(node.scrollTop);
maxScroll = scrollHeight - clientHeight;
delta = deltaY;
hasOverflow = hasOverflowY;
isScrollable = isScrollableY;
hasOverscrollBehavior = hasOverscrollBehaviorY;
} else return false;
if (!hasOverscrollBehavior && (scroll >= maxScroll || scroll <= 0)) return true;
return (delta > 0 ? scroll < maxScroll : scroll > 0) && hasOverflow && isScrollable;
}
/**
* The root element on which lenis is instanced
*/
get rootElement() {
return this.options.wrapper === window ? document.documentElement : this.options.wrapper;
}
/**
* The limit which is the maximum scroll value
*/
get limit() {
if (this.options.naiveDimensions) {
if (this.isHorizontal) return this.rootElement.scrollWidth - this.rootElement.clientWidth;
return this.rootElement.scrollHeight - this.rootElement.clientHeight;
}
return this.dimensions.limit[this.isHorizontal ? "x" : "y"];
}
/**
* Whether or not the scroll is horizontal
*/
get isHorizontal() {
return this.options.orientation === "horizontal";
}
/**
* The actual scroll value
*/
get actualScroll() {
const wrapper = this.options.wrapper;
return this.isHorizontal ? wrapper.scrollX ?? wrapper.scrollLeft : wrapper.scrollY ?? wrapper.scrollTop;
}
/**
* The current scroll value
*/
get scroll() {
return this.options.infinite ? modulo(this.animatedScroll, this.limit) : this.animatedScroll;
}
/**
* The progress of the scroll relative to the limit
*/
get progress() {
return this.limit === 0 ? 1 : this.scroll / this.limit;
}
/**
* Current scroll state
*/
get isScrolling() {
return this._isScrolling;
}
set isScrolling(value) {
if (this._isScrolling !== value) {
this._isScrolling = value;
this.updateClassName();
}
}
/**
* Check if lenis is stopped
*/
get isStopped() {
return this._isStopped;
}
set isStopped(value) {
if (this._isStopped !== value) {
this._isStopped = value;
this.updateClassName();
}
}
/**
* Check if lenis is locked
*/
get isLocked() {
return this._isLocked;
}
set isLocked(value) {
if (this._isLocked !== value) {
this._isLocked = value;
this.updateClassName();
}
}
/**
* Check if lenis is smooth scrolling
*/
get isSmooth() {
return this.isScrolling === "smooth";
}
/**
* The class name applied to the wrapper element
*/
get className() {
let className = "lenis";
if (this.options.autoToggle) className += " lenis-autoToggle";
if (this.isStopped) className += " lenis-stopped";
if (this.isLocked) className += " lenis-locked";
if (this.isScrolling) className += " lenis-scrolling";
if (this.isScrolling === "smooth") className += " lenis-smooth";
return className;
}
updateClassName() {
this.cleanUpClassName();
this.className.split(" ").forEach((className) => {
this.rootElement.classList.add(className);
});
}
cleanUpClassName() {
for (const className of Array.from(this.rootElement.classList)) if (className === "lenis" || className.startsWith("lenis-")) this.rootElement.classList.remove(className);
}
};
//#endregion
export { Lenis as default };
//# sourceMappingURL=lenis.mjs.map