quickshell-web/src/config/styling/marquee.ts

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);
}
});
});