lenis
Version:
How smooth scroll should be
312 lines (308 loc) • 9.39 kB
JavaScript
// packages/snap/src/debounce.ts
function debounce(callback, delay) {
let timer;
return function(...args) {
let context = this;
clearTimeout(timer);
timer = setTimeout(() => {
timer = void 0;
callback.apply(context, args);
}, delay);
};
}
// packages/snap/src/element.ts
function removeParentSticky(element) {
const position = getComputedStyle(element).position;
const isSticky = position === "sticky";
if (isSticky) {
element.style.setProperty("position", "static");
element.dataset.sticky = "true";
}
if (element.offsetParent) {
removeParentSticky(element.offsetParent);
}
}
function addParentSticky(element) {
if (element?.dataset?.sticky === "true") {
element.style.removeProperty("position");
delete element.dataset.sticky;
}
if (element.offsetParent) {
addParentSticky(element.offsetParent);
}
}
function offsetTop(element, accumulator = 0) {
const top = accumulator + element.offsetTop;
if (element.offsetParent) {
return offsetTop(element.offsetParent, top);
}
return top;
}
function offsetLeft(element, accumulator = 0) {
const left = accumulator + element.offsetLeft;
if (element.offsetParent) {
return offsetLeft(element.offsetParent, left);
}
return left;
}
function scrollTop(element, accumulator = 0) {
const top = accumulator + element.scrollTop;
if (element.offsetParent) {
return scrollTop(element.offsetParent, top);
}
return top + window.scrollY;
}
function scrollLeft(element, accumulator = 0) {
const left = accumulator + element.scrollLeft;
if (element.offsetParent) {
return scrollLeft(element.offsetParent, left);
}
return left + window.scrollX;
}
var SnapElement = class {
element;
options;
align;
// @ts-ignore
rect = {};
wrapperResizeObserver;
resizeObserver;
constructor(element, {
align = ["start"],
ignoreSticky = true,
ignoreTransform = false
} = {}) {
this.element = element;
this.options = { align, ignoreSticky, ignoreTransform };
this.align = [align].flat();
this.wrapperResizeObserver = new ResizeObserver(this.onWrapperResize);
this.wrapperResizeObserver.observe(document.body);
this.onWrapperResize();
this.resizeObserver = new ResizeObserver(this.onResize);
this.resizeObserver.observe(this.element);
this.setRect({
width: this.element.offsetWidth,
height: this.element.offsetHeight
});
}
destroy() {
this.wrapperResizeObserver.disconnect();
this.resizeObserver.disconnect();
}
setRect({
top,
left,
width,
height,
element
} = {}) {
top = top ?? this.rect.top;
left = left ?? this.rect.left;
width = width ?? this.rect.width;
height = height ?? this.rect.height;
element = element ?? this.rect.element;
if (top === this.rect.top && left === this.rect.left && width === this.rect.width && height === this.rect.height && element === this.rect.element)
return;
this.rect.top = top;
this.rect.y = top;
this.rect.width = width;
this.rect.height = height;
this.rect.left = left;
this.rect.x = left;
this.rect.bottom = top + height;
this.rect.right = left + width;
}
onWrapperResize = () => {
let top, left;
if (this.options.ignoreSticky) removeParentSticky(this.element);
if (this.options.ignoreTransform) {
top = offsetTop(this.element);
left = offsetLeft(this.element);
} else {
const rect = this.element.getBoundingClientRect();
top = rect.top + scrollTop(this.element);
left = rect.left + scrollLeft(this.element);
}
if (this.options.ignoreSticky) addParentSticky(this.element);
this.setRect({ top, left });
};
onResize = ([entry]) => {
if (!entry?.borderBoxSize[0]) return;
const width = entry.borderBoxSize[0].inlineSize;
const height = entry.borderBoxSize[0].blockSize;
this.setRect({ width, height });
};
};
// packages/snap/src/uid.ts
var index = 0;
function uid() {
return index++;
}
// packages/snap/src/snap.ts
var Snap = class {
constructor(lenis, {
type = "proximity",
lerp,
easing,
duration,
distanceThreshold = "50%",
// velocityThreshold = 1.2,
debounce: debounceDelay = 500,
onSnapStart,
onSnapComplete
} = {}) {
this.lenis = lenis;
this.options = {
type,
lerp,
easing,
duration,
distanceThreshold,
// velocityThreshold,
debounce: debounceDelay,
onSnapStart,
onSnapComplete
};
this.onWindowResize();
window.addEventListener("resize", this.onWindowResize, false);
this.onSnapDebounced = debounce(this.onSnap, this.options.debounce);
this.lenis.on("virtual-scroll", this.onSnapDebounced);
}
options;
elements = /* @__PURE__ */ new Map();
snaps = /* @__PURE__ */ new Map();
viewport = {
width: window.innerWidth,
height: window.innerHeight
};
isStopped = false;
onSnapDebounced;
/**
* Destroy the snap instance
*/
destroy() {
this.lenis.off("virtual-scroll", this.onSnapDebounced);
window.removeEventListener("resize", this.onWindowResize, false);
this.elements.forEach((element) => element.destroy());
}
/**
* Start the snap after it has been stopped
*/
start() {
this.isStopped = false;
}
/**
* Stop the snap
*/
stop() {
this.isStopped = true;
}
/**
* Add a snap to the snap instance
*
* @param value The value to snap to
* @param userData User data that will be forwarded through the snap event
* @returns Unsubscribe function
*/
add(value, userData = {}) {
const id = uid();
this.snaps.set(id, { value, userData });
return () => this.snaps.delete(id);
}
/**
* Add an element to the snap instance
*
* @param element The element to add
* @param options The options for the element
* @returns Unsubscribe function
*/
addElement(element, options = {}) {
const id = uid();
this.elements.set(id, new SnapElement(element, options));
return () => this.elements.delete(id);
}
onWindowResize = () => {
this.viewport.width = window.innerWidth;
this.viewport.height = window.innerHeight;
};
// private onScroll = ({
// // scroll,
// // limit,
// lastVelocity,
// velocity,
// // isScrolling,
// userData,
// }: // isHorizontal,
// Lenis) => {
// if (this.isStopped) return
// // return
// const isDecelerating = Math.abs(lastVelocity) > Math.abs(velocity)
// const isTurningBack =
// Math.sign(lastVelocity) !== Math.sign(velocity) && velocity !== 0
// if (
// Math.abs(velocity) < this.options.velocityThreshold &&
// // !isTouching &&
// isDecelerating &&
// !isTurningBack &&
// userData?.initiator !== 'snap'
// ) {
// this.onSnapDebounced()
// }
// }
onSnap = () => {
let { scroll, isHorizontal } = this.lenis;
scroll = Math.ceil(this.lenis.scroll);
let snaps = [...this.snaps.values()];
this.elements.forEach(({ rect, align }) => {
let value;
align.forEach((align2) => {
if (align2 === "start") {
value = rect.top;
} else if (align2 === "center") {
value = isHorizontal ? rect.left + rect.width / 2 - this.viewport.width / 2 : rect.top + rect.height / 2 - this.viewport.height / 2;
} else if (align2 === "end") {
value = isHorizontal ? rect.left + rect.width - this.viewport.width : rect.top + rect.height - this.viewport.height;
}
if (typeof value === "number") {
snaps.push({ value: Math.ceil(value), userData: {} });
}
});
});
snaps = snaps.sort((a, b) => Math.abs(a.value) - Math.abs(b.value));
if (snaps.length === 0) return;
let prevSnap = snaps.findLast(({ value }) => value <= scroll);
if (prevSnap === void 0) prevSnap = snaps[0];
const distanceToPrevSnap = Math.abs(scroll - prevSnap.value);
let nextSnap = snaps.find(({ value }) => value >= scroll);
if (nextSnap === void 0) nextSnap = snaps[snaps.length - 1];
const distanceToNextSnap = Math.abs(scroll - nextSnap.value);
const snap = distanceToPrevSnap < distanceToNextSnap ? prevSnap : nextSnap;
const distance = Math.abs(scroll - snap.value);
let distanceThreshold;
const axis = isHorizontal ? "width" : "height";
if (typeof this.options.distanceThreshold === "string" && this.options.distanceThreshold.endsWith("%")) {
distanceThreshold = Number(this.options.distanceThreshold.replace("%", "")) / 100 * this.viewport[axis];
} else if (typeof this.options.distanceThreshold === "number") {
distanceThreshold = this.options.distanceThreshold;
} else {
distanceThreshold = this.viewport[axis];
}
if (this.options.type === "mandatory" || this.options.type === "proximity" && distance <= distanceThreshold) {
this.lenis.scrollTo(snap.value, {
lerp: this.options.lerp,
easing: this.options.easing,
duration: this.options.duration,
userData: { initiator: "snap" },
onStart: () => {
this.options.onSnapStart?.(snap);
},
onComplete: () => {
this.options.onSnapComplete?.(snap);
}
});
}
};
};
// packages/snap/browser.ts
globalThis.Snap = Snap;
//# sourceMappingURL=lenis-snap.js.map