refactor: my-marquee abstract component

This commit is contained in:
Tibo De Peuter 2025-05-28 15:04:42 +02:00
parent b90f590e66
commit 76f9dc6499
Signed by: tdpeuter
GPG key ID: 38297DE43F75FFE2
4 changed files with 239 additions and 141 deletions

BIN
assets/sounds/cowbell.mp3 Normal file

Binary file not shown.

View file

@ -2,110 +2,72 @@ function getButtons() {
const buttons = document.createElement('template');
buttons.id = 'button-collection-template';
buttons.innerHTML = `
<style>
.button-collection-marquee-wrapper {
mask-image: linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%);
}
.button-collection-marquee {
overflow: hidden;
overflow-x: auto;
position: relative;
scrollbar-width: none;
}
.button-collection-marquee-content {
display: flex;
gap: 8px;
width: max-content;
animation: scroll-left 60s linear infinite;
}
.button-collection-marquee-content:hover {
animation-play-state: paused;
}
/* Animation */
@keyframes scroll-left {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-25%);
}
}
</style>
<div class="button-collection-marquee-wrapper">
<div class="button-collection-marquee">
<div class="button-collection-marquee-content">
<!-- Socials -->
<button-8831 href="mailto:tibo@depeuter.dev"
src="https://cyber.dabamos.de/88x31/email.gif"
hint="Mail Me"></button-8831>
<button-8831 href="./key.asc"
src="https://www.88x31.nl/gifs/pgp-now.gif"
hint="My GPG key"></button-8831>
<!-- Operating Systems -->
<button-8831 href="https://www.gnu.org/gnu/linux.html"
src="https://www.88x31.nl/gifs/gnu-linux.gif"
hint="Made on GNU/Linux"></button-8831>
<!-- Editors -->
<!-- Services -->
<button-8831 href="https://www.proxmox.com/"
src="https://www.88x31.nl/gifs/proxmox.gif"
hint="Proxmox VE"></button-8831>
<button-8831 href="https://cloud.depeuter.dev/"
src="https://www.88x31.nl/gifs/nextcloud.gif"
hint="Nextcloud"></button-8831>
<button-8831 href="https://ublockorigin.com/"
src="https://www.88x31.nl/gifs/ublock-now.png"
hint="uBlock Origin Now!"></button-8831>
<!-- Movements -->
<button-8831 href="https://home.cern/news/opinion/computing/open-internet-and-free-web"
src="https://www.88x31.nl/gifs/fftake.gif"
hint="Take back the web (Firefox)"></button-8831>
<button-8831 href="https://www.ifixit.com/"
src="https://www.88x31.nl/gifs/right-to-repair.png"
hint="I Support Right To Repair"></button-8831>
<!-- Compatibility -->
<button-8831 href="https://www.mozilla.org/firefox/"
src="https://www.88x31.nl/gifs/firefox4.gif"
hint="Tested on Firefox"></button-8831>
<!-- Web -->
<button-8831 href="https://www.depeuter.dev"
src="https://www.88x31.nl/gifs/graphicdesign.gif"
hint="Graphic Design Is My Passion"></button-8831>
<button-8831 href="https://developer.mozilla.org/en-US/docs/Web/CSS"
src="https://www.88x31.nl/gifs/css.gif"
hint="CSS is awesome"></button-8831>
<!-- Media -->
<button-8831 href="https://last.fm/user/fortemfiducia"
src="https://www.88x31.nl/gifs/3dot5mmfc-button.gif"
hint="My Last.fm account"></button-8831>
<button-8831 href="https://open.spotify.com/user/deyoloboy?si=3bca31169a434d4a"
src="https://www.88x31.nl/gifs/spotify.gif"
hint="My Spotify account"></button-8831>
<button-8831 href="https://www.blankbanshee.com/"
src="https://www.88x31.nl/gifs/banshee.gif"
hint="Blank Banshee"></button-8831>
<button-8831 href="https://fmskyline.bandcamp.com/"
src="https://www.88x31.nl/gifs/fm.gif"
hint="FMSkyline"></button-8831>
<button-8831 href="https://luxuryelite.bandcamp.com/"
src="https://capstasher.neocities.org/88x31Buttons/lu.png"
hint="Luxury Elite"></button-8831>
<button-8831 href="https://nmesh.bandcamp.com/"
src="https://www.88x31.nl/gifs/nm.png"
hint="nmesh"></button-8831>
<!-- Fun -->
<button-8831 href="https://www.depeuter.dev"
src="https://www.88x31.nl/gifs/thedigitalme.gif"
hint="The Digital Me"></button-8831>
</div>
</div>
</div>
`;
<my-marquee speed="50" direction="left">
<!-- Socials -->
<button-8831 href="mailto:tibo@depeuter.dev"
src="https://cyber.dabamos.de/88x31/email.gif"
hint="Mail Me"></button-8831>
<button-8831 href="./key.asc"
src="https://www.88x31.nl/gifs/pgp-now.gif"
hint="My GPG key"></button-8831>
<!-- Operating Systems -->
<button-8831 href="https://www.gnu.org/gnu/linux.html"
src="https://www.88x31.nl/gifs/gnu-linux.gif"
hint="Made on GNU/Linux"></button-8831>
<!-- Editors -->
<!-- Services -->
<button-8831 href="https://www.proxmox.com/"
src="https://www.88x31.nl/gifs/proxmox.gif"
hint="Proxmox VE"></button-8831>
<button-8831 href="https://cloud.depeuter.dev/"
src="https://www.88x31.nl/gifs/nextcloud.gif"
hint="Nextcloud"></button-8831>
<button-8831 href="https://ublockorigin.com/"
src="https://www.88x31.nl/gifs/ublock-now.png"
hint="uBlock Origin Now!"></button-8831>
<!-- Movements -->
<button-8831 href="https://home.cern/news/opinion/computing/open-internet-and-free-web"
src="https://www.88x31.nl/gifs/fftake.gif"
hint="Take back the web (Firefox)"></button-8831>
<button-8831 href="https://www.ifixit.com/"
src="https://www.88x31.nl/gifs/right-to-repair.png"
hint="I Support Right To Repair"></button-8831>
<!-- Compatibility -->
<button-8831 href="https://www.mozilla.org/firefox/"
src="https://www.88x31.nl/gifs/firefox4.gif"
hint="Tested on Firefox"></button-8831>
<!-- Web -->
<button-8831 href="https://www.depeuter.dev"
src="https://www.88x31.nl/gifs/graphicdesign.gif"
hint="Graphic Design Is My Passion"></button-8831>
<button-8831 href="https://developer.mozilla.org/en-US/docs/Web/CSS"
src="https://www.88x31.nl/gifs/css.gif"
hint="CSS is awesome"></button-8831>
<!-- Media -->
<button-8831 href="https://last.fm/user/fortemfiducia"
src="https://www.88x31.nl/gifs/3dot5mmfc-button.gif"
hint="My Last.fm account"></button-8831>
<button-8831 href="https://open.spotify.com/user/deyoloboy?si=3bca31169a434d4a"
src="https://www.88x31.nl/gifs/spotify.gif"
hint="My Spotify account"></button-8831>
<button-8831 href="https://www.blankbanshee.com/"
src="https://www.88x31.nl/gifs/banshee.gif"
hint="Blank Banshee"></button-8831>
<button-8831 href="https://fmskyline.bandcamp.com/"
src="https://www.88x31.nl/gifs/fm.gif"
hint="FMSkyline"></button-8831>
<button-8831 href="https://luxuryelite.bandcamp.com/"
src="https://capstasher.neocities.org/88x31Buttons/lu.png"
hint="Luxury Elite"></button-8831>
<button-8831 href="https://nmesh.bandcamp.com/"
src="https://www.88x31.nl/gifs/nm.png"
hint="nmesh"></button-8831>
<!-- Fun -->
<button-8831 href="https://www.depeuter.dev"
src="https://www.88x31.nl/gifs/thedigitalme.gif"
hint="The Digital Me"></button-8831>
</my-marquee>
`;
return buttons;
}
@ -114,46 +76,11 @@ class ButtonCollection extends HTMLElement {
constructor() {
super();
import ('./button-8831.js');
const templateContent = getButtons().content;
const wrapper = templateContent.cloneNode(true);
const marquee = wrapper.querySelector('.button-collection-marquee');
const content = wrapper.querySelector('.button-collection-marquee-content');
// Duplicate the buttons to create an infinite scroll effect
content.innerHTML = content.innerHTML.repeat(4);
const shadowRoot = this.attachShadow({mode: 'open'});
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.appendChild(wrapper);
window.addEventListener('load', () => {
requestAnimationFrame(() => {
// Half the width of the content
marquee.scrollLeft = content.scrollWidth / 2 - window.innerWidth / 2;
console.log(`Initial scroll position set to: ${marquee.scrollLeft}`);
});
});
marquee.addEventListener('scroll', () => {
const scrollStep = content.scrollWidth / 4; // Half the width of the content
const currentScroll = marquee.scrollLeft;
// Handle scrolling to the right
if (currentScroll >= content.scrollWidth * 0.65) {
console.log(`Jumping back by ${scrollStep}`);
marquee.scrollLeft -= scrollStep; // Jump back by half the width
}
// Handle scrolling to the left
if (currentScroll <= content.scrollWidth * 0.35) {
console.log(`Jumping forward by ${scrollStep}`);
marquee.scrollLeft += scrollStep; // Jump forward by half the width
}
});
}
}

166
components/my-marquee.js Normal file
View file

@ -0,0 +1,166 @@
function getTemplate() {
const template = document.createElement('template');
template.id = 'my-marquee-template';
template.innerHTML = `
<style>
.marquee-wrapper {
mask-image: linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%);
}
.marquee {
overflow: hidden;
overflow-x: auto;
position: relative;
scrollbar-width: none;
}
.marquee-content {
display: flex;
gap: 8px;
width: max-content;
animation: scroll-left 0s linear infinite;
}
.marquee-content:hover {
animation-play-state: paused;
}
/* Animation */
@keyframes scroll-left {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-25%);
}
}
@keyframes scroll-right {
0% {
transform: translateX(0);
}
100% {
transform: translateX(25%);
}
}
</style>
<div class="marquee-wrapper">
<div class="marquee">
<div class="marquee-content">
<slot></slot>
</div>
</div>
</div>
`;
return template;
}
class MyMarquee extends HTMLElement {
static observedAttributes = ['speed', 'direction'];
constructor() {
super();
const wrapper = getTemplate().content.cloneNode(true);
const marquee = wrapper.querySelector('.marquee');
const content = wrapper.querySelector('.marquee-content');
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.appendChild(wrapper);
this.marquee = marquee;
this.content = content;
this.repetitions = 1;
this.speed = 500; // Default speed
this.direction = 'left'; // Default direction
window.addEventListener('load', this.onLoad.bind(this));
this.marquee.addEventListener('scroll', this.handleScroll.bind(this), { passive: true });
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'speed') {
this.speed = parseInt(newValue, 10) || this.speed; // Default to 100 if not set
this.updateAnimation();
} else if (name === 'direction') {
this.direction = newValue || this.direction; // Default to 'left' if not set
this.updateAnimation();
}
}
onLoad() {
const slot = this.content.querySelector('slot');
const slottedElements = slot.assignedNodes({ flatten: true }).filter(node => node.nodeType === Node.ELEMENT_NODE);
this.content.innerHTML = slottedElements.map(el => el.outerHTML).join('');
requestAnimationFrame(() => {
const singleWidth = this.content.scrollWidth;
// Calculate the needed repetitions
let repetitions = Math.ceil(window.innerWidth / singleWidth) + 1;
this.repetitions = repetitions * 4; // Double the repetitions for a seamless effect
if (debug) {
console.log(`Single content width: ${singleWidth}, Repetitions: ${this.repetitions}`);
}
this.content.innerHTML = this.content.innerHTML.repeat(this.repetitions);
this.repeatedWidth = singleWidth * this.repetitions;
this.updateAnimation();
// Set initial scroll position
this.marquee.scrollLeft = (this.repeatedWidth - window.innerWidth) / 2;
});
}
updateAnimation() {
// Set the animation duration
const duration = (this.repeatedWidth / this.speed) * 1000; // Convert to milliseconds
this.content.style.animationDuration = `${duration}ms`;
// Set the animation direction
console.assert(['left', 'right'].includes(this.direction))
this.content.style.animationName = `scroll-${this.direction}`;
if (debug) {
console.log(`Animation updated: Duration ${duration}ms, Direction ${this.direction}`);
}
}
handleScroll() {
const scrollStep = this.content.scrollWidth * 2 / this.repetitions;
const currentScroll = this.marquee.scrollLeft;
// Handle scrolling to the right
if (currentScroll >= this.content.scrollWidth * 0.7) {
if (debug) {
console.log(`Jumping back by ${scrollStep}`);
ping();
}
this.marquee.scrollLeft -= scrollStep;
}
// Handle scrolling to the left
if (currentScroll <= this.content.scrollWidth * 0.3) {
if (debug) {
console.log(`Jumping forward by ${scrollStep}`);
ping();
}
this.marquee.scrollLeft += scrollStep;
}
}
}
function ping() {
const ping = new Audio('../assets/sounds/cowbell.mp3');
ping.play().catch(error => {
console.error('Error playing sound:', error);
});
}
customElements.define('my-marquee', MyMarquee);

View file

@ -1 +1,6 @@
import ('./components/button-collection.js');
import ('./components/button-8831.js');
import ('./components/my-marquee.js');
import ('./components/button-collection.js');
const debug = false;