diff --git a/src/components/marquee/Marquee.astro b/src/components/marquee/Marquee.astro
index b9d3b1e..7695367 100644
--- a/src/components/marquee/Marquee.astro
+++ b/src/components/marquee/Marquee.astro
@@ -14,124 +14,4 @@ import MarqueeContent from "./MarqueeContent.astro";
-
+
diff --git a/src/config/styling/marquee.ts b/src/config/styling/marquee.ts
new file mode 100644
index 0000000..295825e
--- /dev/null
+++ b/src/config/styling/marquee.ts
@@ -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);
+ }
+ });
+});
diff --git a/src/config/styling/marquee_old.ts b/src/config/styling/marquee_old.ts
new file mode 100644
index 0000000..602c79d
--- /dev/null
+++ b/src/config/styling/marquee_old.ts
@@ -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;
+ let vid_containers = document.getElementsByClassName(
+ "marquee-item"
+ ) as HTMLCollectionOf;
+
+ 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;
+ vid_containers = document.getElementsByClassName(
+ "marquee-item"
+ ) as HTMLCollectionOf;
+ 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));
+});
diff --git a/src/config/styling/progressBars.ts b/src/config/styling/progressBars.ts
new file mode 100644
index 0000000..81b5dff
--- /dev/null
+++ b/src/config/styling/progressBars.ts
@@ -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})`;
+});
diff --git a/src/styles/components/marquee.css b/src/styles/components/marquee.css
index 75c9cc1..03fccf2 100644
--- a/src/styles/components/marquee.css
+++ b/src/styles/components/marquee.css
@@ -29,17 +29,18 @@
position: relative;
display: flex;
width: 100%;
- margin-block: var(--lg);
+ margin-block: var(--xl);
justify-content: space-between;
align-items: center;
scroll-snap-type: x mandatory;
}
.marquee-content {
- width: 100%;
height: 100%;
overflow: hidden;
display: flex;
+ will-change: transform;
+ transform: translateX(0);
}
.marquee-item {
@@ -53,7 +54,7 @@
gap: var(--md);
padding-inline: 0.5rem;
- & > * {
+ &>* {
z-index: 11;
}
@@ -98,7 +99,7 @@
flex-direction: column;
justify-content: center;
- & > div {
+ &>div {
width: 2.5rem;
aspect-ratio: 1 / 1;
display: flex;
@@ -112,7 +113,7 @@
&:hover {
cursor: pointer;
- & > div {
+ &>div {
opacity: 0.9;
}
}
@@ -146,7 +147,7 @@
.marquee-scroll-arrow {
height: unset;
- & > div {
+ &>div {
background-color: #55555580;
border-radius: var(--radius-xs);
}
diff --git a/src/styles/css-config/base.css b/src/styles/css-config/base.css
index da93e31..189410b 100644
--- a/src/styles/css-config/base.css
+++ b/src/styles/css-config/base.css
@@ -7,6 +7,8 @@ html {
font-weight: 400;
height: 100svh;
+ width: 100svw;
+ max-width: 100svw;
font-synthesis: none;
text-rendering: optimizeLegibility;