118 lines
3.1 KiB
JavaScript
118 lines
3.1 KiB
JavaScript
// snap.js
|
|
// Full working gesture-based snap-scrolling with wheel, touch and keyboard.
|
|
// The trick: group wheel events into GESTURES, not deltas.
|
|
|
|
(function () {
|
|
|
|
const GESTURE_TIMEOUT = 180; // ms of silence means gesture finished
|
|
const ANIMATION_LOCK = 600; // ms lock to prevent double scrollIntoView
|
|
|
|
let wheelTimeout = null; // end-of-gesture timer
|
|
let cumulativeDelta = 0; // total movement in current gesture
|
|
let animating = false; // lock while animation runs
|
|
|
|
const sections = Array.from(document.querySelectorAll(".section"));
|
|
if (!sections.length) return;
|
|
|
|
const ids = sections.map(s => s.id);
|
|
|
|
function clamp(i) {
|
|
if (i < 0) return 0;
|
|
if (i >= ids.length) return ids.length - 1;
|
|
return i;
|
|
}
|
|
|
|
function getCurrentIndex() {
|
|
let idx = 0;
|
|
let best = Infinity;
|
|
sections.forEach((sec, i) => {
|
|
const dist = Math.abs(sec.getBoundingClientRect().top);
|
|
if (dist < best) {
|
|
best = dist;
|
|
idx = i;
|
|
}
|
|
});
|
|
return idx;
|
|
}
|
|
|
|
function goToSection(index) {
|
|
index = clamp(index);
|
|
animating = true;
|
|
|
|
const target = document.getElementById(ids[index]);
|
|
if (!target) return;
|
|
|
|
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
history.replaceState(null, "", "#" + ids[index]);
|
|
|
|
// unlock after animation ends
|
|
setTimeout(() => { animating = false }, ANIMATION_LOCK);
|
|
}
|
|
|
|
// Wheel handling with gesture grouping
|
|
function handleWheel(e) {
|
|
e.preventDefault();
|
|
if (animating) return;
|
|
|
|
cumulativeDelta += e.deltaY; // keep accumulating in this gesture
|
|
|
|
if (wheelTimeout) clearTimeout(wheelTimeout);
|
|
|
|
// When wheel stops firing, gesture is complete
|
|
wheelTimeout = setTimeout(() => {
|
|
const current = getCurrentIndex();
|
|
const direction = cumulativeDelta > 0 ? 1 : -1;
|
|
goToSection(current + direction);
|
|
|
|
// reset gesture state
|
|
cumulativeDelta = 0;
|
|
wheelTimeout = null;
|
|
|
|
}, GESTURE_TIMEOUT);
|
|
}
|
|
|
|
// Touch gestures
|
|
let startY = 0;
|
|
|
|
function handleTouchStart(e) {
|
|
startY = e.touches[0].clientY;
|
|
}
|
|
|
|
function handleTouchEnd(e) {
|
|
if (animating) return;
|
|
|
|
const endY = e.changedTouches[0].clientY;
|
|
const diff = startY - endY;
|
|
const threshold = 30;
|
|
|
|
if (Math.abs(diff) < threshold) return;
|
|
|
|
const current = getCurrentIndex();
|
|
const direction = diff > 0 ? 1 : -1;
|
|
goToSection(current + direction);
|
|
}
|
|
|
|
// Keyboard input
|
|
function handleKey(e) {
|
|
if (animating) return;
|
|
|
|
const current = getCurrentIndex();
|
|
if (e.key === "ArrowDown" || e.key === "PageDown")
|
|
goToSection(current + 1);
|
|
else if (e.key === "ArrowUp" || e.key === "PageUp")
|
|
goToSection(current - 1);
|
|
else if (e.key === "Home")
|
|
goToSection(0);
|
|
else if (e.key === "End")
|
|
goToSection(ids.length - 1);
|
|
}
|
|
|
|
const container = document.querySelector(".sections") || document;
|
|
|
|
container.addEventListener("wheel", handleWheel, { passive: false });
|
|
container.addEventListener("touchstart", handleTouchStart, { passive: true });
|
|
container.addEventListener("touchend", handleTouchEnd, { passive: true });
|
|
window.addEventListener("keydown", handleKey);
|
|
|
|
})();
|