200 lines
5.2 KiB
TypeScript
200 lines
5.2 KiB
TypeScript
// NOTE: at last index, append every item 1 by 1 starting from 0
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
const container = document.querySelector(
|
|
".marquee-item"
|
|
) as HTMLDivElement;
|
|
const scroller = document.querySelector(
|
|
".marquee-content"
|
|
) as HTMLDivElement;
|
|
if (!scroller) {
|
|
return;
|
|
}
|
|
|
|
const sections = Array.from(
|
|
scroller.querySelectorAll(".marquee-item")
|
|
);
|
|
|
|
const smoothFactor = 0.05;
|
|
const touchSensitivity = 2.5;
|
|
const bufferSize = 2;
|
|
|
|
let targetScrollX = 0;
|
|
let currentScrollX = 0;
|
|
let isAnimating = false;
|
|
|
|
let isDown = false;
|
|
let lastTouchX = 0;
|
|
let touchVelocity = 0;
|
|
let lastTouchTime = 0;
|
|
|
|
const lerp = (start: number, end: number, factor: number) =>
|
|
start + (end - start) * factor;
|
|
|
|
const setupScroll = () => {
|
|
scroller.querySelectorAll(".clone").forEach(clone => {
|
|
clone.remove();
|
|
});
|
|
|
|
const originalSections = Array.from(
|
|
scroller.querySelectorAll(".marquee-item:not(.clone)")
|
|
);
|
|
|
|
const templateSections =
|
|
originalSections.length > 0 ? originalSections : sections;
|
|
|
|
let sequenceWidth = 0;
|
|
templateSections.forEach(section => {
|
|
sequenceWidth += parseFloat(
|
|
window.getComputedStyle(section).width
|
|
);
|
|
});
|
|
|
|
// Create clones before original sections
|
|
for (let i = -bufferSize; i < 0; i++) {
|
|
templateSections.forEach((section, index) => {
|
|
const clone = section.cloneNode(true) as HTMLDivElement;
|
|
clone.classList.add("clone");
|
|
clone.setAttribute("data-clone-index", `${i}-${index}`);
|
|
scroller.appendChild(clone);
|
|
});
|
|
}
|
|
|
|
// Add original sections if none exist
|
|
if (originalSections.length === 0) {
|
|
templateSections.forEach((section, index) => {
|
|
const clone = section.cloneNode(true) as HTMLDivElement;
|
|
clone.setAttribute("data-clone-index", `0-${index}`);
|
|
scroller.appendChild(clone);
|
|
});
|
|
}
|
|
|
|
// Create clones after original sections
|
|
for (let i = 1; i <= bufferSize; i++) {
|
|
templateSections.forEach((section, index) => {
|
|
const clone = section.cloneNode(true) as HTMLDivElement;
|
|
clone.classList.add("clone");
|
|
clone.setAttribute("data-clone-index", `${i}-${index}`);
|
|
scroller.appendChild(clone);
|
|
});
|
|
}
|
|
|
|
scroller.style.width = `${sequenceWidth * (1 + bufferSize * 2)}px`;
|
|
targetScrollX = sequenceWidth * bufferSize;
|
|
currentScrollX = targetScrollX;
|
|
scroller.style.transform = `translateX(-${currentScrollX}px)`;
|
|
|
|
return sequenceWidth;
|
|
};
|
|
|
|
const checkBoundaryAndReset = (sequenceWidth: number) => {
|
|
if (currentScrollX > sequenceWidth * (bufferSize + 0.5)) {
|
|
targetScrollX -= sequenceWidth;
|
|
currentScrollX -= sequenceWidth;
|
|
scroller.style.transform = `translateX(-${currentScrollX}px)`;
|
|
return true;
|
|
}
|
|
|
|
if (currentScrollX < sequenceWidth * (bufferSize - 0.5)) {
|
|
targetScrollX += sequenceWidth;
|
|
currentScrollX += sequenceWidth;
|
|
scroller.style.transform = `translateX(-${currentScrollX}px)`;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
const animate = (
|
|
sequenceWidth: number,
|
|
forceProgressReset = false
|
|
) => {
|
|
currentScrollX = lerp(
|
|
currentScrollX,
|
|
targetScrollX,
|
|
smoothFactor
|
|
);
|
|
scroller.style.transform = `translateX(-${currentScrollX}px)`;
|
|
|
|
if (Math.abs(targetScrollX - currentScrollX) > 0.01) {
|
|
requestAnimationFrame(() => animate(sequenceWidth));
|
|
} else {
|
|
isAnimating = false;
|
|
}
|
|
};
|
|
|
|
// Initialize
|
|
const sequenceWidth = setupScroll();
|
|
|
|
// Wheel event
|
|
container.addEventListener(
|
|
"wheel",
|
|
e => {
|
|
e.preventDefault();
|
|
targetScrollX += e.deltaY;
|
|
|
|
const needsReset = checkBoundaryAndReset(sequenceWidth);
|
|
|
|
if (!isAnimating) {
|
|
isAnimating = true;
|
|
requestAnimationFrame(() =>
|
|
animate(sequenceWidth, needsReset)
|
|
);
|
|
}
|
|
},
|
|
{ passive: false }
|
|
);
|
|
|
|
// Touch events
|
|
container.addEventListener("touchstart", e => {
|
|
isDown = true;
|
|
lastTouchX = e.touches[0].clientX;
|
|
lastTouchTime = Date.now();
|
|
targetScrollX = currentScrollX;
|
|
});
|
|
|
|
container.addEventListener("touchmove", e => {
|
|
if (!isDown) return;
|
|
e.preventDefault();
|
|
|
|
const currentTouchX = e.touches[0].clientX;
|
|
const touchDelta = lastTouchX - currentTouchX;
|
|
|
|
targetScrollX += touchDelta * touchSensitivity;
|
|
|
|
const currentTime = Date.now();
|
|
const timeDelta = currentTime - lastTouchTime;
|
|
if (timeDelta > 0) {
|
|
touchVelocity = (touchDelta / timeDelta) * 15;
|
|
}
|
|
lastTouchX = currentTouchX;
|
|
lastTouchTime = currentTime;
|
|
|
|
const needsReset = checkBoundaryAndReset(sequenceWidth);
|
|
if (!isAnimating) {
|
|
isAnimating = true;
|
|
requestAnimationFrame(() =>
|
|
animate(sequenceWidth, needsReset)
|
|
);
|
|
}
|
|
});
|
|
|
|
container.addEventListener("touchend", () => {
|
|
isDown = false;
|
|
|
|
if (Math.abs(touchVelocity) > 0.1) {
|
|
targetScrollX += touchVelocity * 20;
|
|
|
|
const decayVelocity = () => {
|
|
touchVelocity *= 0.95;
|
|
|
|
if (Math.abs(touchVelocity) > 0.1) {
|
|
targetScrollX += touchVelocity;
|
|
requestAnimationFrame(decayVelocity);
|
|
}
|
|
};
|
|
|
|
requestAnimationFrame(decayVelocity);
|
|
}
|
|
});
|
|
});
|