<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inertia Scrolling with Bounce</title>
<style>
#scrollContainer {
width: 300px;
height: 500px;
overflow: hidden;
border: 1px solid black;
position: relative;
}
#content {
min-height: 1000px;
background: linear-gradient(to bottom, #ffcc00, #ff6600);
display: grid;
grid-gap: 10px;
grid-template-columns: repeat(2, 1fr);
justify-items: center;
}
.item {
height: 100px;
width: 100px;
background: white;
}
</style>
</head>
<body>
<div id="scrollContainer">
<div id="content">
</div>
</div>
<script>
let c = ``;
for(let i = 0; i < 100; i++) {
c += `<div class="item">${i+1}</div>`;
}
document.querySelector('#content').innerHTML = c;
const container = document.getElementById('scrollContainer');
const content = document.getElementById('content');
const contentHeight = content.offsetHeight;
let isDragging = false;
let startY = 0;
let scrollY = 0;
let velocity = 0;
let lastMoveY = 0;
let lastTimestamp = 0;
let animationFrame;
container.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = e.touches[0];
isDragging = true;
startY = touch.clientY;
scrollY = getTranslateY(content);
velocity = 0;
lastMoveY = touch.clientY;
lastTimestamp = e.timeStamp;
cancelAnimationFrame(animationFrame);
}, { passive: false });
document.addEventListener('touchmove', (e) => {
if (!isDragging) return;
e.preventDefault();
const touch = e.touches[0];
const dy = touch.clientY - startY;
content.style.transform = `translateY(${scrollY + dy}px)`;
const now = e.timeStamp;
const dt = now - lastTimestamp;
velocity = (touch.clientY - lastMoveY) / dt * 1000;
lastMoveY = touch.clientY;
lastTimestamp = now;
}, { passive: false });
document.addEventListener('touchend', (e) => {
if (!isDragging) {return}
e.preventDefault();
isDragging = false;
animateInertia();
}, { passive: false });
function animateInertia() {
function step() {
if(Math.abs(velocity) < 60) {
return
}
const deltaY = velocity / 60;
content.style.transform = `translateY(${ getTranslateY(content) + deltaY }px)`;
let y = getTranslateY(content);
if(y >= 60) {
animate(500, easeOutQuad, (value) => {
content.style.transform = `translateY(${ y - y * value }px)`;
})
return;
}
if(y < 0 && Math.abs(y) > contentHeight - container.offsetHeight +60) {
const h = contentHeight - container.offsetHeight;
y = Math.abs(y) - (contentHeight- container.offsetHeight);
animate(500, easeOutQuad, (value) => {
content.style.transform = `translateY(-${ h + (y - y * value) }px)`;
})
return;
}
velocity = velocity * (1- 0.05)
animationFrame = requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
function getTranslateY(element) {
const style = window.getComputedStyle(element);
const transform = style.transform || style.webkitTransform || style.mozTransform;
if (transform && transform !== 'none') {
const match = transform.match(/matrix\(([^,]+),[^,]+,[^,]+,([^,]+),([^,]+),([^,]+)\)/);
if (match) {
return parseFloat(match[4]);
}
}
return 0;
}
function easeOutBack(x) {
const c1 = 1.70158;
const c3 = c1 + 1;
return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2);
}
function easeOutQuad(x) {
return 1 - (1 - x) * (1 - x);
}
function easeOutCirc(x) {
return Math.sqrt(1 - Math.pow(x - 1, 2));
}
function animate(duration = 500, easeFunction, framer) {
let startTime = null;
function step() {
time = Date.now();
if (!startTime) startTime = time;
let progress = (time - startTime) / duration;
if (progress > 1) progress = 1;
let value = easeFunction(progress);
framer(value);
if (progress < 1) {
requestAnimationFrame(step);
}
}
requestAnimationFrame(step);
}
</script>
</body>
</html>