WIP marquee and some refactoring

This commit is contained in:
Oleksandr 2025-12-17 20:13:00 +02:00
parent eca995ca2c
commit 2ca3604414
Signed by: Xanazf
GPG key ID: 821EEC32761AC17C
6 changed files with 406 additions and 127 deletions

View file

@ -14,124 +14,4 @@ import MarqueeContent from "./MarqueeContent.astro";
<MarqueeContent/> <MarqueeContent/>
</div> </div>
<script> <script src="@config/styling/marquee_old.ts"/>
const marquee = document.getElementById("marquee-content")!;
marquee.style.setProperty("--scroll", "0")
marquee.style.setProperty("--mult", "1")
window.addEventListener("load", autoplayInit, false);
let videos = document.getElementsByClassName("marquee-item-content") as HTMLCollectionOf<HTMLVideoElement>;
const videoCount = videos.length;
const lastVideoIndex = videos[videos.length - 1]
let currentVideoIndex = 0;
let currentVideo: HTMLVideoElement | null = null;
function autoplayInit() {
setActiveVideo(0);
currentVideo.play();
currentVideo.style.animationPlayState = "running";
}
function setActiveVideo(index: number) {
if (currentVideo) {
currentVideo.pause();
}
currentVideoIndex = index;
if (index === videos.length - 1) {
console.log("shift")
const shifted = videos.item(0);
marquee.firstElementChild.remove()
const video_div = document.createElement("div");
video_div.classList.add("marquee-item");
console.log("shift: ",video_div.classList.toString())
shifted.setAttribute("data-media-index", (index+1).toString())
video_div.appendChild(shifted);
marquee.appendChild(video_div);
videos = document.getElementsByClassName("marquee-item-content") as HTMLCollectionOf<HTMLVideoElement>;
currentVideoIndex = index - 1;
console.log("shift", marquee)
}
currentVideo = videos[currentVideoIndex];
currentVideo.currentTime = 0;
marquee.style.setProperty("--scroll", `-${currentVideoIndex*100}%`)
marquee.style.setProperty("--mult", `${currentVideoIndex + 1}`)
}
function offsetCarousel(offset: number) {
let nextIndex = currentVideoIndex + offset;
// if (nextIndex < 0) nextIndex += videoCount;
// nextIndex = nextIndex % videoCount;
setActiveVideo(nextIndex);
}
const intersectionOptions = {
root: marquee,
rootMargin: "0px",
threshold: 0.1,
};
const mult = marquee.style.getPropertyValue("--mult") ?? 0;
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
const video = entry.target as HTMLVideoElement;
if (!entry.isIntersecting) {
video.pause();
video.style.animationName = "none";
void video.offsetWidth;
video.style.animationName = "fade";
video.style.animationDuration = "0.3s";
video.style.animationTimingFunction = "ease-in-out";
video.style.animationFillMode = "forwards";
video.style.animationDirection = "reverse";
} else if (video === currentVideo) {
video.play();
video.style.animationName = "none";
void video.offsetWidth;
video.style.animationName = "fade";
video.style.animationDuration = "0.3s";
video.style.animationTimingFunction = "ease-in-out";
video.style.animationFillMode = "forwards";
video.style.animationPlayState = "running";
video.style.animationDirection = "normal";
}
});
}, intersectionOptions);
for (const video of videos) {
observer.observe(video);
video.addEventListener("ended", () => {
// The "ended" event might just mean its buffering.
if (video == currentVideo && video.duration !== 0 && video.currentTime === video.duration) {
offsetCarousel(1);
}
});
}
let wasPaused = false;
document.addEventListener("visibilitychange", () => {
if (currentVideo) {
if (document.hidden) {
wasPaused = currentVideo.paused;
currentVideo.pause();
} else if (!wasPaused) {
currentVideo.play();
}
}
});
// left-right buttons
document.getElementById("marquee-scroll-left")!.addEventListener("mousedown", () => offsetCarousel(-1));
document.getElementById("marquee-scroll-right")!.addEventListener("mousedown", () => offsetCarousel(1));
</script>

View file

@ -0,0 +1,200 @@
// 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);
}
});
});

View file

@ -0,0 +1,144 @@
document.addEventListener("DOMContentLoaded", () => {
const marquee = document.getElementById("marquee-content")!;
marquee.style.setProperty("--scroll", "0");
window.addEventListener("load", autoplayInit, false);
let videos = document.getElementsByClassName(
"marquee-item-content"
) as HTMLCollectionOf<HTMLVideoElement>;
let vid_containers = document.getElementsByClassName(
"marquee-item"
) as HTMLCollectionOf<HTMLDivElement>;
let currentVideoIndex = 0;
let currentVideo: HTMLVideoElement | null = null;
function autoplayInit() {
setActiveVideo(0);
if (currentVideo) {
currentVideo.play();
currentVideo.style.animationPlayState = "running";
}
}
function setActiveVideo(index: number) {
if (currentVideo) {
currentVideo.pause();
}
currentVideoIndex = index;
currentVideo = videos[currentVideoIndex];
currentVideo.currentTime = 0;
marquee.style.setProperty("--scroll", `-${index * 100}%`);
marquee.style.setProperty("--mult", `${index + 1}`);
}
function offsetCarousel(offset: number) {
let nextIndex = currentVideoIndex + offset;
if (nextIndex === videos.length - 1) {
nextIndex = shiftItems(nextIndex);
marquee.style.setProperty(
"--scroll",
`-${(nextIndex - 1) * 100}%`
);
marquee.style.setProperty("--mult", `${nextIndex - 1}`);
}
// NOTE: previous behavior
// nextIndex = nextIndex % videos.length;
setActiveVideo(nextIndex);
}
function shiftItems(index: number) {
const vid_arr = Array.from(vid_containers);
const shifted = vid_arr.shift()! as HTMLDivElement;
shifted.setAttribute("clone", "");
marquee.firstElementChild?.remove();
marquee.appendChild(shifted);
videos = marquee.getElementsByClassName(
"marquee-item-content"
) as HTMLCollectionOf<HTMLVideoElement>;
vid_containers = document.getElementsByClassName(
"marquee-item"
) as HTMLCollectionOf<HTMLDivElement>;
return index - 1;
}
const intersectionOptions = {
root: marquee,
rootMargin: "0px",
threshold: 0.0,
};
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
const video = entry.target as HTMLVideoElement;
if (!entry.isIntersecting) {
video.pause();
video.style.animationName = "none";
void video.offsetWidth;
video.style.animationName = "fade";
video.style.animationDuration = "0.3s";
video.style.animationTimingFunction = "ease-in-out";
video.style.animationFillMode = "forwards";
video.style.animationDirection = "reverse";
} else if (video === currentVideo) {
video.play();
video.style.animationName = "none";
void video.offsetWidth;
video.style.animationName = "fade";
video.style.animationDuration = "0.3s";
video.style.animationTimingFunction = "ease-in-out";
video.style.animationFillMode = "forwards";
video.style.animationPlayState = "running";
video.style.animationDirection = "normal";
}
});
}, intersectionOptions);
for (const video of videos) {
observer.observe(video);
video.addEventListener("ended", () => {
// The "ended" event might just mean its buffering.
if (
video === currentVideo &&
video.duration !== 0 &&
video.currentTime === video.duration
) {
offsetCarousel(1);
}
});
}
let wasPaused = false;
document.addEventListener("visibilitychange", () => {
if (currentVideo) {
if (document.hidden) {
wasPaused = currentVideo.paused;
currentVideo.pause();
} else if (!wasPaused) {
currentVideo.play();
}
}
});
// left-right buttons
document
.getElementById("marquee-scroll-left")!
.addEventListener("mousedown", () => offsetCarousel(-1));
document
.getElementById("marquee-scroll-right")!
.addEventListener("mousedown", () => offsetCarousel(1));
});

View file

@ -0,0 +1,52 @@
document.addEventListener("DOMContentLoaded", () => {
let currentProgressScale = 0;
let targetProgressScale = 0;
let lastPercentage = 0;
const progressCounter = document.querySelector(
".progress-counter h1"
);
const progressBar = document.querySelector(
".progress-bar"
) as HTMLDivElement;
const updateProgress = (
sequenceWidth: number,
forceReset = false
) => {
const basePosition = sequenceWidth * bufferSize;
const currentPosition =
(currentScrollX - basePosition) % sequenceWidth;
let percentage = (currentPosition / sequenceWidth) * 100;
if (percentage < 0) {
percentage = 100 + percentage;
}
const isWrapping =
(lastPercentage > 80 && percentage < 20) ||
(lastPercentage < 20 && percentage > 80) ||
forceReset;
progressCounter.textContent = `${Math.round(percentage)}`;
targetProgressScale = percentage / 100;
if (isWrapping) {
currentProgressScale = targetProgressScale;
progressBar.style.transform = `scaleX(${currentProgressScale})`;
}
lastPercentage = percentage;
};
updateProgress(sequenceWidth, true);
progressBar.style.transform = `scaleX(${currentProgressScale})`;
updateProgress(sequenceWidth, forceProgressReset);
if (!forceProgressReset) {
currentProgressScale = lerp(
currentProgressScale,
targetProgressScale,
smoothFactor
);
}
progressBar.style.transform = `scaleX(${currentProgressScale})`;
});

View file

@ -29,17 +29,18 @@
position: relative; position: relative;
display: flex; display: flex;
width: 100%; width: 100%;
margin-block: var(--lg); margin-block: var(--xl);
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
scroll-snap-type: x mandatory; scroll-snap-type: x mandatory;
} }
.marquee-content { .marquee-content {
width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
will-change: transform;
transform: translateX(0);
} }
.marquee-item { .marquee-item {
@ -53,7 +54,7 @@
gap: var(--md); gap: var(--md);
padding-inline: 0.5rem; padding-inline: 0.5rem;
& > * { &>* {
z-index: 11; z-index: 11;
} }
@ -98,7 +99,7 @@
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
& > div { &>div {
width: 2.5rem; width: 2.5rem;
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
display: flex; display: flex;
@ -112,7 +113,7 @@
&:hover { &:hover {
cursor: pointer; cursor: pointer;
& > div { &>div {
opacity: 0.9; opacity: 0.9;
} }
} }
@ -146,7 +147,7 @@
.marquee-scroll-arrow { .marquee-scroll-arrow {
height: unset; height: unset;
& > div { &>div {
background-color: #55555580; background-color: #55555580;
border-radius: var(--radius-xs); border-radius: var(--radius-xs);
} }

View file

@ -7,6 +7,8 @@ html {
font-weight: 400; font-weight: 400;
height: 100svh; height: 100svh;
width: 100svw;
max-width: 100svw;
font-synthesis: none; font-synthesis: none;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;