Slightly improve scroll behaviour
- Scrolling can snap the section to the screen
This commit is contained in:
parent
dbab8f5770
commit
9ead073ba3
@ -12,12 +12,14 @@ html, body {
|
|||||||
font-family: "IosevkaTermSlab Nerd Font Mono", monospace;
|
font-family: "IosevkaTermSlab Nerd Font Mono", monospace;
|
||||||
background: #202020;
|
background: #202020;
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sections {
|
.sections {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
scroll-snap-type: y mandatory;
|
scroll-snap-type: y mandatory;
|
||||||
|
scroll-snap-stop: always;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Each section is exactly 90vh tall. */
|
/* Each section is exactly 90vh tall. */
|
||||||
|
|||||||
@ -60,6 +60,6 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
<script src="../js/snap.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
117
js/snap.js
Normal file
117
js/snap.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
// 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);
|
||||||
|
|
||||||
|
})();
|
||||||
@ -8,12 +8,14 @@ html, body {
|
|||||||
font-family: "IosevkaTermSlab Nerd Font Mono", monospace;
|
font-family: "IosevkaTermSlab Nerd Font Mono", monospace;
|
||||||
background: v.$bg1;
|
background: v.$bg1;
|
||||||
color: v.$text;
|
color: v.$text;
|
||||||
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sections {
|
.sections {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
scroll-snap-type: y mandatory;
|
scroll-snap-type: y mandatory;
|
||||||
|
scroll-snap-stop: always;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Each section is exactly 90vh tall. */
|
/* Each section is exactly 90vh tall. */
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user