feat: better marquee and js-less theme switch
This commit is contained in:
parent
2ca3604414
commit
da6dd0100b
17 changed files with 689 additions and 831 deletions
|
|
@ -3,6 +3,7 @@ import matrixLogo from "@icons/matrix-logo.svg?raw";
|
|||
import discordLogo from "@icons/discord-logo.svg?raw";
|
||||
import gitLogo from "@icons/git-logo.svg?raw";
|
||||
import { ThemeSelect } from "./hooks/ThemeSwitch";
|
||||
import ThemeToggle from "./ThemeToggle.astro";
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
|
|
@ -19,7 +20,7 @@ const props = Astro.props;
|
|||
and our contributors
|
||||
</a>
|
||||
</div>
|
||||
<ThemeSelect client:load />
|
||||
<ThemeToggle />
|
||||
<div class="socials-changelog">
|
||||
<section class="socials">
|
||||
<a href="https://matrix.to/#/#quickshell:outfoxxed.me" target="_blank" aria-label="Join our matrix space">
|
||||
|
|
|
|||
59
src/components/ThemeToggle.astro
Normal file
59
src/components/ThemeToggle.astro
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
---
|
||||
|
||||
<label class="theme-toggle icon-button standard" title="Toggle theme">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="theme-manual-toggle"
|
||||
class="theme-toggle-input"
|
||||
aria-label="Toggle theme (light/dark)"
|
||||
/>
|
||||
<Icon
|
||||
name="sun"
|
||||
class="light-icon"
|
||||
style="width: 24px; height: 24px;"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Icon
|
||||
name="moon"
|
||||
class="dark-icon"
|
||||
style="width: 24px; height: 24px;"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="state-layer"></div>
|
||||
</label>
|
||||
|
||||
<style>
|
||||
.theme-toggle {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.theme-toggle-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.light-icon {
|
||||
display: block;
|
||||
}
|
||||
.dark-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.theme-toggle:has(.theme-toggle-input:checked) .light-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.theme-toggle:has(.theme-toggle-input:checked) .dark-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.theme-toggle:focus-within {
|
||||
outline: 2px solid var(--accent-600);
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
---
|
||||
// NOTE: to be migrated to @config/styling/animations_helper.ts
|
||||
---
|
||||
<script>
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// NOTE: to be replaced by @components/ThemeToggle.astro
|
||||
import {
|
||||
createSignal,
|
||||
createEffect,
|
||||
|
|
|
|||
|
|
@ -14,4 +14,4 @@ import MarqueeContent from "./MarqueeContent.astro";
|
|||
<MarqueeContent/>
|
||||
</div>
|
||||
|
||||
<script src="@config/styling/marquee_old.ts"/>
|
||||
<script src="@config/styling/marquee.ts"/>
|
||||
|
|
|
|||
29
src/config/styling/animations_helper.ts
Normal file
29
src/config/styling/animations_helper.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
export function initAnimations() {
|
||||
const observerOptions = {
|
||||
root: null,
|
||||
rootMargin: "0px",
|
||||
threshold: 0.1,
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add("visible");
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
observerOptions
|
||||
);
|
||||
|
||||
const animatedElements = document.querySelectorAll(
|
||||
".animate-fade-up, .stagger-parent"
|
||||
);
|
||||
animatedElements.forEach(el => observer.observe(el));
|
||||
}
|
||||
|
||||
// auto-init on DOMContentLoaded
|
||||
if (typeof document !== "undefined") {
|
||||
document.addEventListener("DOMContentLoaded", initAnimations);
|
||||
}
|
||||
|
|
@ -1,200 +1,226 @@
|
|||
// 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 container = document.querySelector(".marquee") as HTMLDivElement;
|
||||
const scroller = document.querySelector(".marquee-content") as HTMLDivElement;
|
||||
const btnLeft = document.getElementById("marquee-scroll-left");
|
||||
const btnRight = document.getElementById("marquee-scroll-right");
|
||||
|
||||
const sections = Array.from(
|
||||
scroller.querySelectorAll(".marquee-item")
|
||||
);
|
||||
if (!container || !scroller) return;
|
||||
|
||||
const smoothFactor = 0.05;
|
||||
const touchSensitivity = 2.5;
|
||||
const bufferSize = 2;
|
||||
let items = Array.from(
|
||||
scroller.querySelectorAll(".marquee-item")
|
||||
) as HTMLDivElement[];
|
||||
const originalCount = items.length;
|
||||
if (originalCount === 0) return;
|
||||
|
||||
let itemWidth = 0;
|
||||
let sequenceWidth = 0;
|
||||
let targetScrollX = 0;
|
||||
let currentScrollX = 0;
|
||||
let isAnimating = false;
|
||||
|
||||
let isDown = false;
|
||||
let lastTouchX = 0;
|
||||
let touchVelocity = 0;
|
||||
let lastTouchTime = 0;
|
||||
const smoothFactor = 0.1;
|
||||
const snapThreshold = 0.1;
|
||||
|
||||
// setup clones
|
||||
const setupClones = () => {
|
||||
// remove existing clones
|
||||
scroller.querySelectorAll(".clone").forEach((c) => c.remove());
|
||||
|
||||
const originals = Array.from(scroller.querySelectorAll(".marquee-item")) as HTMLDivElement[];
|
||||
|
||||
// add clones after
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
originals.forEach((item) => {
|
||||
const clone = item.cloneNode(true) as HTMLDivElement;
|
||||
clone.classList.add("clone");
|
||||
scroller.appendChild(clone);
|
||||
});
|
||||
}
|
||||
|
||||
// add clones before
|
||||
const beforeContainer = document.createDocumentFragment();
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
originals.forEach((item) => {
|
||||
const clone = item.cloneNode(true) as HTMLDivElement;
|
||||
clone.classList.add("clone");
|
||||
beforeContainer.appendChild(clone);
|
||||
});
|
||||
}
|
||||
scroller.insertBefore(beforeContainer, scroller.firstChild);
|
||||
|
||||
items = Array.from(scroller.querySelectorAll(".marquee-item")) as HTMLDivElement[];
|
||||
};
|
||||
|
||||
const updateDimensions = () => {
|
||||
itemWidth = container.clientWidth;
|
||||
if (itemWidth === 0) return;
|
||||
|
||||
sequenceWidth = originalCount * itemWidth;
|
||||
|
||||
// standardize width
|
||||
scroller.style.width = `${items.length * itemWidth}px`;
|
||||
items.forEach((item) => {
|
||||
item.style.width = `${itemWidth}px`;
|
||||
item.style.flex = `0 0 ${itemWidth}px`;
|
||||
item.style.maxWidth = `${itemWidth}px`;
|
||||
});
|
||||
|
||||
targetScrollX = bufferSize * sequenceWidth + (targetScrollX % sequenceWidth);
|
||||
currentScrollX = targetScrollX;
|
||||
scroller.style.transform = `translateX(-${currentScrollX}px)`;
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
const animate = () => {
|
||||
if (!isDown && Math.abs(touchVelocity) < 0.1) {
|
||||
// snap to nearest item if not interacting and close to one
|
||||
const nearestItemScroll = Math.round(targetScrollX / itemWidth) * itemWidth;
|
||||
if (Math.abs(targetScrollX - nearestItemScroll) < itemWidth * 0.5) {
|
||||
targetScrollX = lerp(targetScrollX, nearestItemScroll, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
currentScrollX = lerp(currentScrollX, targetScrollX, smoothFactor);
|
||||
|
||||
// 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;
|
||||
// boundary reset
|
||||
if (currentScrollX > (bufferSize + 1) * sequenceWidth) {
|
||||
currentScrollX -= sequenceWidth;
|
||||
scroller.style.transform = `translateX(-${currentScrollX}px)`;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentScrollX < sequenceWidth * (bufferSize - 0.5)) {
|
||||
targetScrollX += sequenceWidth;
|
||||
targetScrollX -= sequenceWidth;
|
||||
} else if (currentScrollX < (bufferSize - 1) * sequenceWidth) {
|
||||
currentScrollX += sequenceWidth;
|
||||
scroller.style.transform = `translateX(-${currentScrollX}px)`;
|
||||
return true;
|
||||
targetScrollX += sequenceWidth;
|
||||
}
|
||||
|
||||
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));
|
||||
// fade in-out and scale items based on distance from center
|
||||
items.forEach((item, index) => {
|
||||
const itemCenter = index * itemWidth;
|
||||
const distance = Math.abs(currentScrollX - itemCenter);
|
||||
const progress = Math.min(distance / itemWidth, 1); // 0 at center, 1 at edge
|
||||
|
||||
const opacity = 1 - progress;
|
||||
const scale = 1 - progress * 0.1; // scale down as it leaves
|
||||
const yOffset = progress * 20; // slide down as it leaves
|
||||
|
||||
item.style.opacity = opacity.toString();
|
||||
// NOTE: apply transform to the video container specifically
|
||||
// to keep layout stable
|
||||
const content = item.querySelector(".marquee-item-content") as HTMLElement;
|
||||
if (content) {
|
||||
content.style.transform = `scale(${scale}) translateY(${yOffset}px)`;
|
||||
}
|
||||
});
|
||||
|
||||
const diff = Math.abs(targetScrollX - currentScrollX);
|
||||
const interaction = isDown || Math.abs(touchVelocity) > 0.1;
|
||||
|
||||
if (diff > snapThreshold || interaction) {
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
isAnimating = false;
|
||||
currentScrollX = targetScrollX;
|
||||
scroller.style.transform = `translateX(-${currentScrollX}px)`;
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize
|
||||
const sequenceWidth = setupScroll();
|
||||
const startAnimation = () => {
|
||||
if (!isAnimating) {
|
||||
isAnimating = true;
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
// Wheel event
|
||||
container.addEventListener(
|
||||
"wheel",
|
||||
e => {
|
||||
e.preventDefault();
|
||||
targetScrollX += e.deltaY;
|
||||
// video handling
|
||||
const videos = scroller.querySelectorAll("video");
|
||||
const observerOptions = {
|
||||
root: container,
|
||||
threshold: 0.5,
|
||||
};
|
||||
|
||||
const needsReset = checkBoundaryAndReset(sequenceWidth);
|
||||
|
||||
if (!isAnimating) {
|
||||
isAnimating = true;
|
||||
requestAnimationFrame(() =>
|
||||
animate(sequenceWidth, needsReset)
|
||||
);
|
||||
const videoObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
const video = entry.target as HTMLVideoElement;
|
||||
if (entry.isIntersecting) {
|
||||
video.play().catch(() => {}); // Handle potential autoplay blocks
|
||||
} else {
|
||||
video.pause();
|
||||
}
|
||||
},
|
||||
{ passive: false }
|
||||
);
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
// Touch events
|
||||
container.addEventListener("touchstart", e => {
|
||||
videos.forEach((v) => {
|
||||
videoObserver.observe(v);
|
||||
v.addEventListener("ended", () => {
|
||||
targetScrollX += itemWidth;
|
||||
startAnimation();
|
||||
});
|
||||
});
|
||||
|
||||
// events
|
||||
btnLeft?.addEventListener("click", () => {
|
||||
targetScrollX -= itemWidth;
|
||||
startAnimation();
|
||||
});
|
||||
|
||||
btnRight?.addEventListener("click", () => {
|
||||
targetScrollX += itemWidth;
|
||||
startAnimation();
|
||||
});
|
||||
|
||||
container.addEventListener("wheel", (e) => {
|
||||
e.preventDefault();
|
||||
targetScrollX += e.deltaY;
|
||||
startAnimation();
|
||||
}, { passive: false });
|
||||
|
||||
container.addEventListener("touchstart", (e) => {
|
||||
isDown = true;
|
||||
lastTouchX = e.touches[0].clientX;
|
||||
lastTouchTime = Date.now();
|
||||
targetScrollX = currentScrollX;
|
||||
touchVelocity = 0;
|
||||
});
|
||||
|
||||
container.addEventListener("touchmove", e => {
|
||||
container.addEventListener("touchmove", (e) => {
|
||||
if (!isDown) return;
|
||||
e.preventDefault();
|
||||
|
||||
const currentTouchX = e.touches[0].clientX;
|
||||
const touchDelta = lastTouchX - currentTouchX;
|
||||
const deltaX = lastTouchX - currentTouchX;
|
||||
targetScrollX += deltaX * 1.5;
|
||||
|
||||
targetScrollX += touchDelta * touchSensitivity;
|
||||
const now = Date.now();
|
||||
const dt = now - lastTouchTime;
|
||||
if (dt > 0) touchVelocity = deltaX / dt;
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
lastTouchTime = now;
|
||||
startAnimation();
|
||||
});
|
||||
|
||||
container.addEventListener("touchend", () => {
|
||||
isDown = false;
|
||||
targetScrollX += touchVelocity * 100; // Momentum
|
||||
touchVelocity = 0;
|
||||
startAnimation();
|
||||
});
|
||||
|
||||
if (Math.abs(touchVelocity) > 0.1) {
|
||||
targetScrollX += touchVelocity * 20;
|
||||
window.addEventListener("resize", updateDimensions);
|
||||
|
||||
const decayVelocity = () => {
|
||||
touchVelocity *= 0.95;
|
||||
|
||||
if (Math.abs(touchVelocity) > 0.1) {
|
||||
targetScrollX += touchVelocity;
|
||||
requestAnimationFrame(decayVelocity);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(decayVelocity);
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.hidden) {
|
||||
videos.forEach(v => v.pause());
|
||||
}
|
||||
});
|
||||
|
||||
// init
|
||||
setupClones();
|
||||
setTimeout(() => {
|
||||
updateDimensions();
|
||||
startAnimation();
|
||||
}, 50);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,144 +0,0 @@
|
|||
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));
|
||||
});
|
||||
|
|
@ -19,6 +19,9 @@ const { title, description } = Astro.props;
|
|||
<body class="baselayout">
|
||||
<!--<Header />-->
|
||||
<slot />
|
||||
<script>
|
||||
import "@config/styling/animations_helper.ts";
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
|
|||
|
|
@ -30,29 +30,31 @@
|
|||
display: flex;
|
||||
width: 100%;
|
||||
margin-block: var(--xl);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
scroll-snap-type: x mandatory;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.marquee-content {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
will-change: transform;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.marquee-item {
|
||||
position: relative;
|
||||
flex: 1 0 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
transition: left 0.3s var(--ease-in-out);
|
||||
left: var(--scroll);
|
||||
gap: var(--md);
|
||||
padding-inline: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
will-change: opacity;
|
||||
|
||||
&>* {
|
||||
z-index: 11;
|
||||
|
|
@ -72,32 +74,37 @@
|
|||
|
||||
.marquee-item-content {
|
||||
border-radius: var(--radius-sm);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.marquee-scroll {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
max-width: 85rem;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
transition:
|
||||
background-color 0.3s,
|
||||
opacity 0.3s;
|
||||
z-index: 20;
|
||||
user-select: none;
|
||||
align-items: stretch;
|
||||
pointer-events: none;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.marquee-scroll-arrow {
|
||||
max-width: 8rem;
|
||||
width: 8rem;
|
||||
font-size: 2rem;
|
||||
pointer-events: all;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
&>div {
|
||||
width: 2.5rem;
|
||||
|
|
@ -152,9 +159,5 @@
|
|||
border-radius: var(--radius-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.marquee-scroll {
|
||||
width: 92%;
|
||||
left: 4%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ html {
|
|||
position: relative;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
transition: all 0.3s var(--ease-in-out);
|
||||
/* transition: all 0.15s var(--ease-in-out); */
|
||||
}
|
||||
|
||||
body {
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@
|
|||
--footer-bkg-border: var(--blue) 32% 84%;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
html:has(input#theme-manual-toggle:checked) {
|
||||
/* accent */
|
||||
--green: 141deg;
|
||||
--accent-400: var(--green) 100% 67%;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue