/* ============================================== FONT ============================================== */ @font-face { font-family: 'Abel'; font-style: normal; font-weight: 400; font-display: swap; src: url('https://fonts.gstatic.com/s/abel/v18/MwQ5bhbm2POE6VhLPJp6qGI.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* ============================================== CARD PRINCIPALE ============================================== */ .ag-video-card-wrapper { position: relative; width: 100%; height: 400px; margin: 0; padding: 0; } .ag-video-card { position: relative; display: block; width: 100%; height: 100%; padding: 0; border: none; background: transparent; overflow: hidden; cursor: pointer; } .ag-video-card:focus-visible { outline: 3px solid #FD8B00; outline-offset: 4px; } /* ============================================== IMMAGINE ============================================== */ .ag-card-image { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-size: cover; background-repeat: no-repeat; background-position: right center; transition: transform 0.6s cubic-bezier(0.25, 1, 0.5, 1), filter 0.3s ease; } .ag-card-image.ag-loaded { background-image: var(--ag-bg-url); } /* ============================================== OVERLAY ============================================== */ .ag-card-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0); transition: background-color 0.4s ease-out; display: flex; justify-content: center; align-items: center; z-index: 10; } .ag-card-title { color: #d47502; font-family: 'Abel', sans-serif; font-size: 25px; font-weight: 400; text-align: center; margin: 0; transform: translateY(20px); opacity: 0; transition: transform 0.4s ease-out, opacity 0.4s ease-out; transition-delay: 0.1s; } /* ============================================== HOVER — identico all'originale funzionante ============================================== */ .ag-video-card:hover .ag-card-image, .ag-video-card:focus-visible .ag-card-image { transform: scale(1.05); filter: blur(2px) sepia(60%) hue-rotate(340deg); } .ag-video-card:hover .ag-card-overlay, .ag-video-card:focus-visible .ag-card-overlay { background-color: rgba(0, 0, 0, 0.5); } .ag-video-card:hover .ag-card-title, .ag-video-card:focus-visible .ag-card-title { opacity: 1; transform: translateY(0); } /* ============================================== LIGHTBOX MODAL ============================================== */ .ag-video-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.9); display: flex; justify-content: center; align-items: center; z-index: 999999; padding: 20px; opacity: 0; animation: agFadeIn 0.3s ease forwards; } @keyframes agFadeIn { to { opacity: 1; } } .ag-modal-content { position: relative; width: 100%; max-width: 1100px; aspect-ratio: 16 / 9; background: #000; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); animation: agScaleIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; } @keyframes agScaleIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } } .ag-modal-content iframe { width: 100%; height: 100%; border: none; } /* Pulsante Chiusura */ .ag-modal-close { position: absolute; top: -50px; right: 0; width: 32px !important; height: 32px !important; min-width: 32px !important; min-height: 32px !important; max-width: 32px !important; max-height: 32px !important; padding: 0 !important; background: #FD8B00 !important; border: none !important; border-radius: 50% !important; color: #fff !important; font-size: 20px !important; line-height: 32px !important; font-family: Arial, sans-serif !important; cursor: pointer; transition: background 0.2s ease; z-index: 1; display: flex !important; align-items: center !important; justify-content: center !important; } .ag-modal-close:hover { background: #ff721a !important; } .ag-modal-close:focus-visible { outline: 3px solid #fff; outline-offset: 4px; } /* Loading Spinner */ .ag-modal-loading { position: absolute; inset: 0; display: flex; justify-content: center; align-items: center; background: #000; } .ag-spinner { width: 50px; height: 50px; border: 4px solid rgba(255, 255, 255, 0.1); border-top-color: #FD8B00; border-radius: 50%; animation: agSpin 0.8s linear infinite; } @keyframes agSpin { to { transform: rotate(360deg); } } /* ============================================== RESPONSIVE - TABLET e MOBILE (touch) ============================================== */ @media (max-width: 1024px) { .ag-card-title { opacity: 1; transform: translateY(0); } .ag-card-overlay { background-color: rgba(0, 0, 0, 0); } } /* ============================================== RESPONSIVE - MOBILE ============================================== */ @media (max-width: 768px) { .ag-video-card-wrapper { height: 300px; } .ag-modal-close { top: 10px; right: 10px; } .ag-card-title { font-size: 20px; writing-mode: vertical-rl; } .ag-card-overlay { align-items: flex-end; padding-bottom: 10px; } } /* ============================================== RESPONSIVE - LANDSCAPE MOBILE ============================================== */ @media (orientation: landscape) and (max-height: 500px) { .ag-video-modal { padding: 0; } .ag-modal-content { width: 100vw; max-width: 100vw; height: 100vh; height: 100dvh; max-height: 100vh; max-height: 100dvh; aspect-ratio: unset; } .ag-modal-close { top: 8px !important; right: 8px !important; opacity: 0.85; z-index: 10; } } /* Fallback per browser senza aspect-ratio */ @supports not (aspect-ratio: 16 / 9) { .ag-modal-content { padding-bottom: 56.25%; height: 0; } .ag-modal-content iframe { position: absolute; top: 0; left: 0; } } (function () { 'use strict'; // SINGLETON if (window.AGVideoCard) { window.AGVideoCard.setup(); return; } const AGVideoCard = { initialized: false, config: { providers: { vimeo: { validate: (id) => /^\d+$/.test(id), getEmbedUrl: (id) => `https://player.vimeo.com/video/${id}?dnt=1&autoplay=1` }, youtube: { validate: (id) => /^[a-zA-Z0-9_-]{11}$/.test(id), getEmbedUrl: (id) => `https://www.youtube-nocookie.com/embed/${id}?rel=0&autoplay=1` } } }, init() { if (this.initialized) return; this.initialized = true; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.setup()); } else { this.setup(); } }, setup() { document.querySelectorAll('.ag-video-card').forEach(card => { if (card.dataset.agInitialized) return; card.dataset.agInitialized = 'true'; this.loadImage(card); card.addEventListener('click', (e) => this.handleCardClick(e)); card.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.handleCardClick(e); } }); }); }, loadImage(card) { const imageDiv = card.querySelector('.ag-card-image'); const bgUrl = imageDiv?.dataset.bg; if (!bgUrl) return; const img = new Image(); img.onload = () => { imageDiv.style.setProperty('--ag-bg-url', `url('${bgUrl}')`); imageDiv.classList.add('ag-loaded'); }; img.onerror = () => console.error('[AG Video Card] Errore immagine:', bgUrl); img.src = bgUrl; }, handleCardClick(e) { const card = e.currentTarget; const videoId = card.dataset.videoId; const provider = card.dataset.videoProvider || 'vimeo'; if (!this.validateVideo(videoId, provider)) { console.error('[AG Video Card] ID video non valido:', videoId); return; } this.openModal(videoId, provider); }, validateVideo(id, provider) { const p = this.config.providers[provider]; return p && p.validate(id); }, openModal(videoId, provider) { document.querySelectorAll('.ag-video-modal').forEach(m => { const f = m.querySelector('iframe'); if (f) f.src = ''; m.remove(); }); const embedUrl = this.config.providers[provider].getEmbedUrl(videoId); const modal = document.createElement('div'); modal.className = 'ag-video-modal'; modal.setAttribute('role', 'dialog'); modal.setAttribute('aria-modal', 'true'); modal.setAttribute('aria-label', 'Video player'); modal.innerHTML = `
`; document.body.appendChild(modal); document.body.style.overflow = 'hidden'; // src dopo appendChild — fix schermo nero Android Chrome const iframe = modal.querySelector('iframe'); const loading = modal.querySelector('.ag-modal-loading'); const removeSpinner = () => { if (loading?.parentNode) loading.remove(); }; iframe.addEventListener('load', removeSpinner); setTimeout(removeSpinner, 3000); iframe.src = embedUrl; this.setupModalClose(modal); this.setupFocusTrap(modal); }, setupModalClose(modal) { modal.querySelector('.ag-modal-close') .addEventListener('click', () => this.closeModal(modal)); modal.addEventListener('click', (e) => { if (e.target === modal) this.closeModal(modal); }); const escHandler = (e) => { if (e.key === 'Escape') { this.closeModal(modal); document.removeEventListener('keydown', escHandler); } }; document.addEventListener('keydown', escHandler); }, closeModal(modal) { const iframe = modal.querySelector('iframe'); if (iframe) iframe.src = ''; modal.style.animation = 'agFadeIn 0.2s ease reverse'; setTimeout(() => { if (modal.parentNode) modal.parentNode.removeChild(modal); document.body.style.overflow = ''; }, 200); }, setupFocusTrap(modal) { const focusable = modal.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); const first = focusable[0]; const last = focusable[focusable.length - 1]; setTimeout(() => first?.focus(), 100); modal.addEventListener('keydown', (e) => { if (e.key !== 'Tab') return; if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } }); } }; AGVideoCard.init(); window.AGVideoCard = AGVideoCard; if (window.MutationObserver) { new MutationObserver((mutations) => { const hasNew = mutations.some(m => [...m.addedNodes].some(n => n.nodeType === 1 && ( n.classList?.contains('ag-video-card-wrapper') || n.querySelector?.('.ag-video-card-wrapper') ) ) ); if (hasNew) AGVideoCard.setup(); }).observe(document.body, { childList: true, subtree: true }); } })();
// Mini-script: ri-inizializza le nuove card (function() { if (window.AGVideoCard) { // Se il sistema principale è già caricato, ri-scansiona setTimeout(() => window.AGVideoCard.setup(), 100); } else { // Se non è ancora caricato, aspetta e riprova const checkInterval = setInterval(() => { if (window.AGVideoCard) { window.AGVideoCard.setup(); clearInterval(checkInterval); } }, 100); } })();

This space is part of my

life.

stories, projects and the way I shape them.

a long journey

filled with attempt, struggles, falls,

successes,

fears and laughter.

My goal:

crafting

emotions.

In a film, a documentary, or in a thirty-second commercial.

FEATURES

documentaries

Image Accordion Widget /* ============================================ * 🎨 CONFIGURAZIONE WIDGET - MODIFICA QUI * ============================================ */ const ACCORDION_CONFIG = { // ===== IMPOSTAZIONI GENERALI ===== totalImages: 4, borderRadius: '12px', // ===== GAP TRA PANNELLI (non genera bordo sul pannello espanso) ===== panelGap: { desktop: '5px', // >= 768px mobile: '2px' // = 1400px desktop_l: 6, // Schermi >= 1200px desktop_m: 5, // Schermi >= 992px tablet: 4, // Schermi >= 768px mobile: 3 // Schermi < 768px }, // ===== ANIMAZIONI ===== transitionDuration: '0.6s', zoomDuration: '0.8s', zoomScale: 1.08, // ===== COLORI ===== backgroundColor: '#DAE1DF', overlayBackground: 'rgba(0, 0, 0, 0.5)', textColor: '#ffffff', // ===== FONT ===== titleFont: "'Abel', sans-serif", titleSize: '2.0rem', titleWeight: '400', descriptionFont: "'Inter', sans-serif", descriptionSize: '1rem', descriptionWeight: '400', // ===== IMMAGINI ===== images: [ { path: 'https://albertogelpi.com/wp-content/uploads/2026/02/doc_hanbury3.jpg', title: '

BOTANICAL GARDENS', description: 'Menton, Antibes, Hanbury:
a voyage across Planet Earth.', link: 'https://albertogelpi.com/index.php/botanical-gardens/' }, { path: 'https://albertogelpi.com/wp-content/uploads/2026/02/doc_heritage4.jpg', title: '

HERITAGE', description: 'The Italian hearth of Scotland.', link: 'https://albertogelpi.com/index.php/heritage/' }, { path: 'https://albertogelpi.com/wp-content/uploads/2026/03/doc_aip2.jpg', title: '

AFFIANCO', description: 'Lives in the Mirror:
(extra)ordinary stories of
Primary Immunodeficiencies.', link: 'https://albertogelpi.com/index.php/affianco/' }, { path: 'https://albertogelpi.com/wp-content/uploads/2026/03/doc_cronache-animali2.jpg', title: '

ANIMAL CHRONICLES', description: 'Stories of men and animals
in the North.', link: 'https://albertogelpi.com/index.php/animal_chronicles/' }, ] }; /* ============================================ CSS CUSTOM PROPERTIES ============================================ */ :root { --transition-duration: 0.6s; --transition-timing: cubic-bezier(0.4, 0, 0.2, 1); --zoom-duration: 0.8s; --zoom-scale: 1.08; --overlay-bg: rgba(0, 0, 0, 0.5); --text-color: #ffffff; --nav-button-bg: rgba(255, 255, 255, 0.9); --nav-button-hover: rgba(255, 255, 255, 1); --border-radius: 10px; --panel-gap: 5px; /* Aggiornato via JS — spazio TRA pannelli */ --background-color: #DAE1DF; --title-font: 'Abel', sans-serif; --title-size: 2.5rem; --title-weight: 700; --description-font: 'Inter', sans-serif; --description-size: 1rem; --description-weight: 400; } /* ============================================ RESET & BASE STYLES ============================================ */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; line-height: 1.6; background-color: #f5f5f5; padding: 20px; } /* ============================================ ACCORDION CONTAINER ============================================ */ .accordion-container { max-width: 1400px; margin: 0 auto; position: relative; overflow: hidden; background: var(--background-color); border-radius: 12px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1); } .accordion-wrapper { position: relative; width: 100%; height: 500px; overflow: hidden; } /* ============================================ ACCORDION TRACK gap: crea spazio SOLO tra pannelli adiacenti, mai sui bordi esterni → nessun bordo visivo sul pannello espanso ============================================ */ .accordion-track { display: flex; height: 100%; gap: var(--panel-gap); } /* ============================================ PANEL STYLES ============================================ */ .accordion-panel { position: relative; flex: 1; height: 100%; overflow: hidden; cursor: pointer; transition: flex var(--transition-duration) var(--transition-timing); border-radius: var(--border-radius); } .accordion-panel.expanded { flex: 3; } .accordion-panel.hidden { display: none; } .accordion-panel a { display: block; width: 100%; height: 100%; text-decoration: none; color: inherit; position: relative; } /* Immagine occupa l'intero pannello — zero inset → zero bordo visivo */ .panel-image { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-size: cover; background-position: center; background-repeat: no-repeat; transition: transform var(--zoom-duration) cubic-bezier(0.25, 0.46, 0.45, 0.94); border-radius: var(--border-radius); } .accordion-panel:hover .panel-image { transform: scale(var(--zoom-scale)); } /* Overlay scuro */ .panel-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: var(--overlay-bg); opacity: 0; transition: opacity var(--transition-duration) var(--transition-timing); display: flex; align-items: center; justify-content: center; padding: 20px; pointer-events: none; } .accordion-panel.expanded .panel-overlay { opacity: 1; } /* Testo overlay */ .panel-text { color: var(--text-color); text-align: center; opacity: 0; transform: translateY(20px); transition: all var(--transition-duration) var(--transition-timing); display: flex; flex-direction: column; gap: 4px; } .accordion-panel.expanded .panel-text { opacity: 1; transform: translateY(0); } .panel-title { font-family: var(--title-font); font-size: var(--title-size); font-weight: var(--title-weight); text-transform: uppercase; letter-spacing: 2px; line-height: 1.2; } .panel-title h2 { margin: 0; } .panel-description { font-family: var(--description-font); font-size: var(--description-size); font-weight: var(--description-weight); letter-spacing: 0.5px; line-height: 1.5; opacity: 0.9; } /* ============================================ NAVIGATION CONTROLS ============================================ */ .nav-controls { position: absolute; top: 50%; transform: translateY(-50%); width: 100%; height: 0; pointer-events: none; z-index: 10; } .nav-button { pointer-events: all; position: absolute; top: 50%; transform: translateY(-50%); width: 50px; height: 50px; border: none; border-radius: 50%; background: var(--nav-button-bg); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } #prevBtn { left: 20px; } #nextBtn { right: 20px; } .nav-button:hover:not(:disabled) { background: var(--nav-button-hover); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2); } #prevBtn:hover:not(:disabled) { transform: translateY(-50%) scale(1.1); } #nextBtn:hover:not(:disabled) { transform: translateY(-50%) scale(1.1); } .nav-button:disabled { display: none; } .nav-button svg { width: 24px; height: 24px; fill: #333; } /* ============================================ DOTS INDICATOR ============================================ */ .dots-indicator { display: flex; gap: 10px; align-items: center; justify-content: center; padding: 20px 0; max-width: 1400px; margin: 0 auto; } .dot { width: 12px; height: 12px; flex-shrink: 0; border-radius: 50%; background: rgba(255, 255, 255, 0.3); border: 2px solid rgba(255, 255, 255, 0.5); cursor: pointer; transition: all 0.3s ease; padding: 0; } .dot.active { background: rgba(255, 255, 255, 1); border-color: rgba(255, 255, 255, 1); width: 14px; height: 14px; } .dot:hover { background: rgba(255, 255, 255, 0.6); } /* ============================================ RESPONSIVE ============================================ */ @media (min-width: 768px) and (max-width: 991px) { .accordion-wrapper { height: 400px; } .panel-title { font-size: 2rem; } .panel-description { font-size: 0.9rem; } } @media (max-width: 767px) { body { padding: 0; } .accordion-container { border-radius: 0; box-shadow: none; max-width: 100%; } .accordion-wrapper { height: 380px; } .nav-controls { display: none; } .panel-title { font-size: 1.3rem; letter-spacing: 1px; } .panel-description { font-size: 0.8rem; } .panel-overlay { padding: 12px; } .dots-indicator { padding: 14px 0; } } /* ============================================ ACCESSIBILITY ============================================ */ .accordion-panel:focus-within { outline: 3px solid #4A90E2; outline-offset: 2px; } .nav-button:focus, .dot:focus { outline: 2px solid #4A90E2; outline-offset: 2px; } /* ============================================ UTILITIES ============================================ */ .no-select { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; }
class ImageAccordion { constructor(config) { this.config = config; this.container = document.getElementById('accordionContainer'); this.track = document.getElementById('accordionTrack'); this.prevBtn = document.getElementById('prevBtn'); this.nextBtn = document.getElementById('nextBtn'); this.dotsIndicator = document.getElementById('dotsIndicator'); this.currentBlock = 0; this.expandedPanelIndex = 0; this.isDesktop = this.detectDesktop(); this.touchStartX = 0; this.touchEndX = 0; this.isDragging = false; this.init(); } detectDesktop() { return window.innerWidth >= 992; } /* -------------------------------------------------- applyCSSVariables Applica tutte le variabili CSS dalla config. Il gap viene gestito separatamente da applyResponsiveGap() per supportare valori diversi per breakpoint. -------------------------------------------------- */ applyCSSVariables() { const root = document.documentElement; root.style.setProperty('--transition-duration', this.config.transitionDuration); root.style.setProperty('--zoom-duration', this.config.zoomDuration); root.style.setProperty('--zoom-scale', this.config.zoomScale); root.style.setProperty('--overlay-bg', this.config.overlayBackground); root.style.setProperty('--text-color', this.config.textColor); root.style.setProperty('--border-radius', this.config.borderRadius); root.style.setProperty('--background-color', this.config.backgroundColor); root.style.setProperty('--title-font', this.config.titleFont); root.style.setProperty('--title-size', this.config.titleSize); root.style.setProperty('--title-weight', this.config.titleWeight); root.style.setProperty('--description-font', this.config.descriptionFont); root.style.setProperty('--description-size', this.config.descriptionSize); root.style.setProperty('--description-weight', this.config.descriptionWeight); // Applica il gap responsive this.applyResponsiveGap(); } /* -------------------------------------------------- applyResponsiveGap Imposta --panel-gap in base alla larghezza corrente. Desktop >= 768px → panelGap.desktop Mobile = 768 ? this.config.panelGap.desktop : this.config.panelGap.mobile; document.documentElement.style.setProperty('--panel-gap', gap); } init() { this.applyCSSVariables(); this.updatePanelsPerBlock(); this.renderPanels(); // Nascondi pannelli non del primo blocco this.track.querySelectorAll('.accordion-panel').forEach((panel, index) => { if (index >= this.panelsPerBlock) panel.classList.add('hidden'); }); this.renderDots(); this.attachEventListeners(); this.updateNavigation(); this.expandPanel(0); } updatePanelsPerBlock() { if (this.config.fixedPanelsPerBlock) { this.panelsPerBlock = this.config.fixedPanelsPerBlock; } else { const w = window.innerWidth; if (w >= 1400) this.panelsPerBlock = this.config.panelsPerBlock.desktop_xl; else if (w >= 1200) this.panelsPerBlock = this.config.panelsPerBlock.desktop_l; else if (w >= 992) this.panelsPerBlock = this.config.panelsPerBlock.desktop_m; else if (w >= 768) this.panelsPerBlock = this.config.panelsPerBlock.tablet; else this.panelsPerBlock = this.config.panelsPerBlock.mobile; } this.totalBlocks = Math.ceil(this.config.images.length / this.panelsPerBlock); } renderPanels() { this.track.innerHTML = ''; this.config.images.forEach((image, index) => { const panel = document.createElement('div'); panel.className = 'accordion-panel'; panel.dataset.index = index; panel.innerHTML = `
${image.title}
${image.description}
`; this.track.appendChild(panel); }); } renderDots() { this.dotsIndicator.innerHTML = ''; if (this.totalBlocks <= 1) { this.dotsIndicator.style.display = 'none'; return; } this.dotsIndicator.style.display = 'flex'; for (let i = 0; i this.goToBlock(i)); this.dotsIndicator.appendChild(dot); } } attachEventListeners() { this.prevBtn.addEventListener('click', () => this.previousBlock()); this.nextBtn.addEventListener('click', () => this.nextBlock()); // Desktop: hover if (this.isDesktop) { this.track.addEventListener('mouseover', (e) => { const panel = e.target.closest('.accordion-panel'); if (panel && !panel.classList.contains('hidden')) { const index = parseInt(panel.dataset.index); const blockIndex = Math.floor(index / this.panelsPerBlock); if (blockIndex === this.currentBlock) this.expandPanel(index); } }); } // Mobile/Tablet: click this.track.addEventListener('click', (e) => { if (!this.isDesktop) { e.preventDefault(); const panel = e.target.closest('.accordion-panel'); if (panel) { const index = parseInt(panel.dataset.index); const blockIndex = Math.floor(index / this.panelsPerBlock); if (blockIndex !== this.currentBlock) this.goToBlock(blockIndex); this.expandPanel(index); } } }); // Swipe this.track.addEventListener('touchstart', (e) => { this.touchStartX = e.changedTouches[0].screenX; this.isDragging = true; }, { passive: true }); this.track.addEventListener('touchend', (e) => { if (!this.isDragging) return; this.touchEndX = e.changedTouches[0].screenX; this.handleSwipe(); this.isDragging = false; }, { passive: true }); // Resize let resizeTimeout; window.addEventListener('resize', () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { // Aggiorna gap responsive this.applyResponsiveGap(); const oldPanelsPerBlock = this.panelsPerBlock; const wasDesktop = this.isDesktop; this.updatePanelsPerBlock(); this.isDesktop = this.detectDesktop(); if (oldPanelsPerBlock !== this.panelsPerBlock) { this.currentBlock = Math.floor(this.expandedPanelIndex / this.panelsPerBlock); this.renderDots(); this.updateNavigation(); this.updatePanelVisibility(); } }, 250); }); // Keyboard document.addEventListener('keydown', (e) => { if (e.key === 'ArrowLeft') this.previousBlock(); else if (e.key === 'ArrowRight') this.nextBlock(); }); } handleSwipe() { const diff = this.touchStartX - this.touchEndX; if (Math.abs(diff) > 50) { if (diff > 0) this.nextBlock(); else this.previousBlock(); } } expandPanel(index) { this.track.querySelectorAll('.accordion-panel').forEach(p => p.classList.remove('expanded')); const target = this.track.querySelector(`[data-index="${index}"]`); if (target) { target.classList.add('expanded'); this.expandedPanelIndex = index; } } previousBlock() { if (this.currentBlock > 0) this.goToBlock(this.currentBlock - 1); } nextBlock() { if (this.currentBlock < this.totalBlocks - 1) this.goToBlock(this.currentBlock + 1); } goToBlock(blockIndex) { if (blockIndex = this.totalBlocks) return; this.currentBlock = blockIndex; this.updatePanelVisibility(); this.updateNavigation(); this.updateDots(); this.expandPanel(blockIndex * this.panelsPerBlock); } updatePanelVisibility() { const allPanels = this.track.querySelectorAll('.accordion-panel'); const startIndex = this.currentBlock * this.panelsPerBlock; const endIndex = Math.min(startIndex + this.panelsPerBlock, this.config.images.length); const duration = parseFloat(this.config.transitionDuration) * 1000; // Fade out this.track.style.transition = `opacity ${this.config.transitionDuration} ease-out`; this.track.style.opacity = '0'; setTimeout(() => { allPanels.forEach((panel, index) => { panel.classList.remove('expanded'); if (index >= startIndex && index { this.track.style.transition = `opacity ${this.config.transitionDuration} ease-in`; this.track.style.opacity = '1'; this.expandPanel(startIndex); }); setTimeout(() => { this.track.style.transition = ''; this.track.style.opacity = ''; }, duration); }, duration); } updateNavigation() { this.prevBtn.disabled = this.currentBlock === 0; this.nextBtn.disabled = this.currentBlock === this.totalBlocks - 1; } updateDots() { this.dotsIndicator.querySelectorAll('.dot').forEach((dot, index) => { dot.classList.toggle('active', index === this.currentBlock); }); } } document.addEventListener('DOMContentLoaded', () => { if (ACCORDION_CONFIG.images.length !== ACCORDION_CONFIG.totalImages) { console.warn(`⚠️ totalImages (${ACCORDION_CONFIG.totalImages}) non corrisponde alle immagini fornite (${ACCORDION_CONFIG.images.length})`); } const accordion = new ImageAccordion(ACCORDION_CONFIG); window.accordion = accordion; console.log('✅ Image Accordion inizializzato'); });

commercials

/* ==== VARIABILI CSS ==== */ :root { --gallery-bg: #000000; --overlay-color: rgba(0, 0, 0, 0.15); --transition-speed: 0.9s; } /* ==== STILE BASE ==== */ .scrolling-gallery { overflow: hidden; width: 100%; background-color: var(--gallery-bg); padding: 5px 0; position: relative; isolation: isolate; cursor: grab; } .scrolling-gallery.is-dragging { cursor: grabbing; user-select: none; } /* ==== RIGA SCORRIMENTO ==== */ .scrolling-row { display: flex; flex-wrap: nowrap; width: auto; will-change: transform; } /* ==== CONTENITORE IMMAGINI ==== */ .image-wrapper { position: relative; display: inline-block; margin-right: 15px; border-radius: var(--border-radius); overflow: hidden; flex-shrink: 0; cursor: pointer; } .is-dragging .image-wrapper { pointer-events: none; } /* ==== IMMAGINE BASE ==== */ .image-wrapper img { aspect-ratio: 16 / 9; width: 25vw; height: auto; object-fit: cover; display: block; transition: transform var(--transition-speed) cubic-bezier(0.25, 1, 0.5, 1), filter 0.4s ease; will-change: filter, transform; user-select: none; -webkit-user-drag: none; } /* ==== OVERLAY ARANCIONE ==== */ .image-wrapper::before { content: ''; position: absolute; inset: 0; background-color: var(--overlay-color); opacity: 0; filter: saturate(100%) sepia(100%) hue-rotate(340deg) brightness(.4) saturate(100%); transition: opacity 1s ease-out; pointer-events: none; z-index: 1; } /* ==== TESTO HOVER ==== */ .hover-text { position: absolute; top: 50%; left: 50%; font-family: 'Abel'; font-size: 12px; font-weight: bold; letter-spacing: 2px; color: #FD8B00; text-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); white-space: nowrap; opacity: 0; transform: translate(-50%, -50%); transition: opacity 0.5s ease; pointer-events: none; z-index: 2; will-change: opacity; background-color: rgba(0, 0, 0, 0.45); padding: 3px 3px; } .hover-text .letter { display: inline-block; } /* ==== STATO HOVER ==== */ .image-wrapper:hover::before { opacity: 1; } .image-wrapper:hover .hover-text { opacity: 1; } .image-wrapper:hover img { transform: scale(1.1); filter: blur(2px) sepia(60%) hue-rotate(340deg) brightness(0.5); } .image-wrapper:focus-visible { outline: 3px solid #fff; outline-offset: 2px; } /* ==== LIGHTBOX MODAL — sistema film card ==== */ .ag-video-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.9); display: flex; justify-content: center; align-items: center; z-index: 999999; padding: 20px; opacity: 0; animation: agFadeIn 0.3s ease forwards; } @keyframes agFadeIn { to { opacity: 1; } } .ag-modal-content { position: relative; width: 100%; max-width: 1100px; aspect-ratio: 16 / 9; background: #000; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); animation: agScaleIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; } @keyframes agScaleIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } } .ag-modal-content iframe { width: 100%; height: 100%; border: none; } .ag-modal-close { position: absolute; top: -50px; right: 0; width: 32px !important; height: 32px !important; min-width: 32px !important; min-height: 32px !important; max-width: 32px !important; max-height: 32px !important; padding: 0 !important; background: #FD8B00 !important; border: none !important; border-radius: 50% !important; color: #fff !important; font-size: 20px !important; line-height: 32px !important; font-family: Arial, sans-serif !important; cursor: pointer; transition: background 0.2s ease; z-index: 1; display: flex !important; align-items: center !important; justify-content: center !important; } .ag-modal-close:hover { background: #ff721a !important; } .ag-modal-close:focus-visible { outline: 3px solid #fff; outline-offset: 4px; } /* Loading Spinner */ .ag-modal-loading { position: absolute; inset: 0; display: flex; justify-content: center; align-items: center; background: #000; } .ag-spinner { width: 50px; height: 50px; border: 4px solid rgba(255, 255, 255, 0.1); border-top-color: #FD8B00; border-radius: 50%; animation: agSpin 0.8s linear infinite; } @keyframes agSpin { to { transform: rotate(360deg); } } body.ag-modal-open { overflow: hidden; } /* ==== RESPONSIVE ==== */ @media (max-width: 1024px) { .scrolling-gallery, .scrolling-gallery.is-dragging { cursor: default; } .image-wrapper img { width: 35vw; } .hover-text { opacity: 1; } } @media (max-width: 768px) { .image-wrapper img { width: 50vw; } .ag-modal-close { top: 10px !important; right: 10px !important; width: 40px !important; height: 40px !important; font-size: 28px !important; line-height: 40px !important; position: fixed !important; } } @media (max-width: 480px) { .image-wrapper img { width: 70vw; } } @media (orientation: landscape) and (max-height: 500px) { .ag-video-modal { padding: 0; } .ag-modal-content { width: 100vw; max-width: 100vw; height: 100vh; height: 100dvh; aspect-ratio: unset; } .ag-modal-close { top: 8px !important; right: 8px !important; position: fixed !important; opacity: 0.85; } } @supports not (aspect-ratio: 16 / 9) { .ag-modal-content { padding-bottom: 56.25%; height: 0; } .ag-modal-content iframe { position: absolute; top: 0; left: 0; } } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } (function () { 'use strict'; window.GelpiGallery = window.GelpiGallery || { instances: [], initialized: false }; const CONFIG = { vimeoParams: '?dnt=1&autoplay=1&title=0&byline=0&portrait=0', minDecryptDuration: 40, maxDecryptDuration: 100, decryptFrameRate: 8, directionThreshold: 6, dragSensitivity: 1.0, hoverDebounce: 50 }; const CHARS = '▀▄█▌▐░▒▓@#$%&*+=?'; function debounce(func, wait) { let timeout; return function (...args) { clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; } // ===== LIGHTBOX — sistema film card ===== const GelpiModal = { open(vimeoUrl) { document.querySelectorAll('.ag-video-modal').forEach(m => { const f = m.querySelector('iframe'); if (f) f.src = ''; m.remove(); }); const videoId = this.extractVimeoId(vimeoUrl); if (!videoId) { console.error('[GelpiGallery] ID Vimeo non valido:', vimeoUrl); return; } const embedUrl = `https://player.vimeo.com/video/${videoId}${CONFIG.vimeoParams}`; const modal = document.createElement('div'); modal.className = 'ag-video-modal'; modal.setAttribute('role', 'dialog'); modal.setAttribute('aria-modal', 'true'); modal.setAttribute('aria-label', 'Video player'); modal.innerHTML = `
`; document.body.appendChild(modal); document.body.classList.add('ag-modal-open'); const openTime = Date.now(); // ← FIX: timestamp apertura const iframe = modal.querySelector('iframe'); const loading = modal.querySelector('.ag-modal-loading'); const removeSpinner = () => { if (loading?.parentNode) loading.remove(); }; iframe.addEventListener('load', removeSpinner); setTimeout(removeSpinner, 3000); iframe.src = embedUrl; this._setupClose(modal, openTime); // ← FIX: passa timestamp setTimeout(() => modal.querySelector('.ag-modal-close').focus(), 100); }, close(modal) { const iframe = modal.querySelector('iframe'); if (iframe) iframe.src = ''; modal.style.animation = 'agFadeIn 0.2s ease reverse'; setTimeout(() => { if (modal.parentNode) modal.parentNode.removeChild(modal); document.body.classList.remove('ag-modal-open'); }, 200); }, _setupClose(modal, openTime) { modal.querySelector('.ag-modal-close') .addEventListener('click', () => this.close(modal)); modal.addEventListener('click', (e) => { if (Date.now() - openTime { if (e.key === 'Escape') { this.close(modal); document.removeEventListener('keydown', escHandler); } }; document.addEventListener('keydown', escHandler); }, extractVimeoId(url) { if (!url) return null; if (/^\d+$/.test(url)) return url; const m = url.match(/(?:vimeo\.com\/(?:video\/)?|player\.vimeo\.com\/video\/)(\d+)/); return m ? m[1] : null; } }; // ===== SCROLLING GALLERY ===== class ScrollingGallery { constructor(galleryElement, instanceId) { this.gallery = galleryElement; this.instanceId = instanceId; this.row = this.gallery.querySelector('.scrolling-row'); this.loopWidth = 0; this.dragState = { active: false, direction: null, startX: 0, startY: 0, currentX: 0, translateX: 0, hasMoved: false, startTarget: null }; this.animationState = { isAnimating: true, isPaused: false, currentPosition: 0, animationFrame: null, speed: 0, lastTime: null }; this.activeAnimations = new Map(); this.boundHandlers = {}; this.init(); } init() { this.setupInfiniteLoop(); this.calculateInitialSpeed(); this.attachDragEvents(); this.attachWrapperEvents(); this.prefillTextForTouch(); this.attachHoverPauseEvents(); this.attachVisibilityHandler(); this.startManualAnimation(); console.log(`[GelpiGallery] Istanza #${this.instanceId} inizializzata — loopWidth: ${this.loopWidth}px`); } // ===== AUTO-CLONE LOOP INFINITO ===== setupInfiniteLoop() { const originalItems = Array.from(this.row.children); if (originalItems.length === 0) return; let originalWidth = 0; originalItems.forEach(item => { const style = getComputedStyle(item); originalWidth += item.offsetWidth + parseFloat(style.marginRight || 0); }); this.loopWidth = originalWidth; const containerWidth = this.gallery.offsetWidth; const minTotalWidth = containerWidth * 3; const copiesNeeded = Math.ceil(minTotalWidth / originalWidth) + 1; for (let i = 0; i { const clone = item.cloneNode(true); clone.setAttribute('aria-hidden', 'true'); clone.removeAttribute('tabindex'); ScrollingGallery.prepareTextEl(clone.querySelector('.hover-text')); // ← aggiunto this.row.appendChild(clone); }); } } calculateInitialSpeed() { const durationStr = this.row.style.animationDuration || getComputedStyle(this.row).animationDuration; const duration = parseFloat(durationStr) || 70; const direction = this.row.classList.contains('scroll-left') ? -1 : 1; this.animationState.speed = (this.loopWidth / duration) * direction; } startManualAnimation() { const animate = (currentTime) => { if (!this.animationState.isAnimating) return; if (this.animationState.lastTime === null) { this.animationState.lastTime = currentTime; } const delta = (currentTime - this.animationState.lastTime) / 1000; this.animationState.lastTime = currentTime; if (!this.animationState.isPaused && !this.dragState.active) { this.animationState.currentPosition += this.animationState.speed * delta; if (this.animationState.speed < 0) { if (this.animationState.currentPosition = 0) { this.animationState.currentPosition -= this.loopWidth; } } this.row.style.transform = `translate3d(${this.animationState.currentPosition}px, 0, 0)`; } this.animationState.animationFrame = requestAnimationFrame(animate); }; this.animationState.animationFrame = requestAnimationFrame(animate); } stopManualAnimation() { this.animationState.isAnimating = false; if (this.animationState.animationFrame) { cancelAnimationFrame(this.animationState.animationFrame); this.animationState.animationFrame = null; } } normalizePosition() { if (this.animationState.speed 0) this.animationState.currentPosition -= this.loopWidth; while (this.animationState.currentPosition < -this.loopWidth) this.animationState.currentPosition += this.loopWidth; } else { while (this.animationState.currentPosition 0) this.animationState.currentPosition -= this.loopWidth; } } // ===== DRAG / TOUCH ===== attachDragEvents() { this.boundHandlers.mouseMove = this.onMouseMove.bind(this); this.boundHandlers.mouseUp = this.onMouseUp.bind(this); this.boundHandlers.touchMove = this.onTouchMove.bind(this); this.boundHandlers.touchEnd = this.onTouchEnd.bind(this); this.gallery.addEventListener('mousedown', (e) => this.onMouseDown(e)); this.gallery.addEventListener('mouseleave', (e) => { if (this.dragState.active) this.onMouseUp(e); }); this.gallery.addEventListener('touchstart', (e) => this.onTouchStart(e), { passive: false }); } onMouseDown(e) { if (e.target.closest('a, button')) return; e.preventDefault(); this.dragState.active = true; this.dragState.direction = 'horizontal'; this.dragState.startX = e.pageX; this.dragState.currentX = e.pageX; this.dragState.hasMoved = false; this.dragState.startTarget = e.target; this.dragState.translateX = this.animationState.currentPosition; this.gallery.classList.add('is-dragging'); document.addEventListener('mousemove', this.boundHandlers.mouseMove); document.addEventListener('mouseup', this.boundHandlers.mouseUp); } onMouseMove(e) { if (!this.dragState.active) return; e.preventDefault(); const delta = (e.pageX - this.dragState.currentX) * CONFIG.dragSensitivity; this.dragState.currentX = e.pageX; this.dragState.translateX += delta; if (Math.abs(e.pageX - this.dragState.startX) > CONFIG.directionThreshold) { this.dragState.hasMoved = true; } this.row.style.transform = `translate3d(${this.dragState.translateX}px, 0, 0)`; } onMouseUp(e) { if (!this.dragState.active) return; this.dragState.active = false; this.gallery.classList.remove('is-dragging'); document.removeEventListener('mousemove', this.boundHandlers.mouseMove); document.removeEventListener('mouseup', this.boundHandlers.mouseUp); if (!this.dragState.hasMoved && this.dragState.startTarget) { this._openFromTarget(this.dragState.startTarget); } if (this.dragState.hasMoved) { this.animationState.currentPosition = this.dragState.translateX; this.normalizePosition(); this.animationState.lastTime = null; } this.dragState.startTarget = null; if (!this.gallery.matches(':hover')) { this.animationState.isPaused = false; } } onTouchStart(e) { if (e.touches.length !== 1) return; const t = e.touches[0]; this.dragState.active = false; this.dragState.direction = null; this.dragState.startX = t.pageX; this.dragState.startY = t.pageY; this.dragState.currentX = t.pageX; this.dragState.hasMoved = false; this.dragState.startTarget = e.target; this.dragState.translateX = this.animationState.currentPosition; document.addEventListener('touchmove', this.boundHandlers.touchMove, { passive: false }); document.addEventListener('touchend', this.boundHandlers.touchEnd, { passive: true }); document.addEventListener('touchcancel', this.boundHandlers.touchEnd, { passive: true }); } onTouchMove(e) { if (e.touches.length !== 1) return; const t = e.touches[0]; const absX = Math.abs(t.pageX - this.dragState.startX); const absY = Math.abs(t.pageY - this.dragState.startY); if (this.dragState.direction === null) { if (absX < CONFIG.directionThreshold && absY = absY ? 'horizontal' : 'vertical'; } // FIX: qualsiasi movimento oltre soglia annulla il tap, anche verticale if (absX > CONFIG.directionThreshold || absY > CONFIG.directionThreshold) { this.dragState.hasMoved = true; } if (this.dragState.direction === 'vertical') return; e.preventDefault(); if (!this.dragState.active) { this.dragState.active = true; this.gallery.classList.add('is-dragging'); this.animationState.isPaused = true; } const delta = (t.pageX - this.dragState.currentX) * CONFIG.dragSensitivity; this.dragState.currentX = t.pageX; this.dragState.translateX += delta; this.dragState.hasMoved = true; this.row.style.transform = `translate3d(${this.dragState.translateX}px, 0, 0)`; } onTouchEnd(e) { document.removeEventListener('touchmove', this.boundHandlers.touchMove); document.removeEventListener('touchend', this.boundHandlers.touchEnd); document.removeEventListener('touchcancel', this.boundHandlers.touchEnd); if (!this.dragState.hasMoved && this.dragState.startTarget) { this._openFromTarget(this.dragState.startTarget); } if (this.dragState.active) { this.gallery.classList.remove('is-dragging'); if (this.dragState.hasMoved) { this.animationState.currentPosition = this.dragState.translateX; this.normalizePosition(); this.animationState.lastTime = null; } } this.dragState.active = false; this.dragState.direction = null; this.dragState.startTarget = null; this.animationState.isPaused = false; } _openFromTarget(target) { const wrapper = target.closest('.image-wrapper'); if (!wrapper) return; const vimeoUrl = wrapper.querySelector('img')?.getAttribute('data-vimeo'); if (vimeoUrl) GelpiModal.open(vimeoUrl); } // ===== HOVER PAUSE ===== attachHoverPauseEvents() { this.gallery.addEventListener('mouseenter', () => { if (!this.dragState.active) this.animationState.isPaused = true; }, { passive: true }); this.gallery.addEventListener('mouseleave', () => { if (!this.dragState.active) { this.animationState.isPaused = false; this.animationState.lastTime = null; } }, { passive: true }); } // ===== TESTO DECRYPT ===== attachWrapperEvents() { const allWrappers = Array.from(this.row.querySelectorAll('.image-wrapper')); allWrappers.forEach(wrapper => { const textEl = wrapper.querySelector('.hover-text'); const debouncedEnter = debounce(() => { if (!this.dragState.active) { this.activeAnimations.set(wrapper, this.initializeAndAnimateText(textEl)); } }, CONFIG.hoverDebounce); wrapper.addEventListener('mouseenter', debouncedEnter, { passive: true }); wrapper.addEventListener('mouseleave', () => { this.activeAnimations.delete(wrapper); this.resetText(textEl); }, { passive: true }); // Keyboard solo sugli originali (i cloni non hanno tabindex) if (!wrapper.hasAttribute('aria-hidden')) { wrapper.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this._openFromTarget(wrapper); } }); } }); } initializeAndAnimateText(textEl) { const text = textEl.getAttribute('data-text'); if (!text) return; textEl.innerHTML = text.split('').map(c => c === ' ' ? ` ` : `` ).join(''); const nonSpaceIndices = []; text.split('').forEach((c, i) => { if (c !== ' ') nonSpaceIndices.push(i); }); const shuffled = nonSpaceIndices.sort(() => Math.random() - 0.5); const letterEls = textEl.querySelectorAll('.letter'); const animations = shuffled.map((origIdx, pos) => { const duration = CONFIG.minDecryptDuration + Math.random() * (CONFIG.maxDecryptDuration - CONFIG.minDecryptDuration); return this.decryptLetter(letterEls[origIdx], text[origIdx], pos * 50, duration); }); return Promise.all(animations); } decryptLetter(el, target, delay, duration) { return new Promise(resolve => { setTimeout(() => { const end = Date.now() + duration; const iv = setInterval(() => { if (Date.now() >= end || el.textContent === target) { el.textContent = target; clearInterval(iv); resolve(); } else { el.textContent = target === ' ' ? '\u00A0' : CHARS[Math.floor(Math.random() * CHARS.length)]; } }, CONFIG.decryptFrameRate); }, delay); }); } resetText(textEl) { const text = textEl.getAttribute('data-text'); if (text) { textEl.innerHTML = text.split('').map(c => c === ' ' ? ` ` : `` ).join(''); } } prefillTextForTouch() { if (window.innerWidth > 1024) return; Array.from(this.row.querySelectorAll('.image-wrapper')).forEach(wrapper => { const textEl = wrapper.querySelector('.hover-text'); const text = textEl?.getAttribute('data-text'); if (!text) return; textEl.innerHTML = text.split('').map(c => c === ' ' ? ` ` : `${c}` ).join(''); }); } static prepareTextEl(textEl) { const text = textEl?.getAttribute('data-text'); if (!text) return; textEl.innerHTML = text.split('').map(c => c === ' ' ? ` ` : `` ).join(''); } // ===== VISIBILITY ===== attachVisibilityHandler() { const handler = () => { if (!this.dragState.active) { this.animationState.isPaused = document.hidden; if (!document.hidden) this.animationState.lastTime = null; } }; document.addEventListener('visibilitychange', handler); this.boundHandlers.visibilityHandler = handler; } // ===== CLEANUP ===== destroy() { document.removeEventListener('mousemove', this.boundHandlers.mouseMove); document.removeEventListener('mouseup', this.boundHandlers.mouseUp); document.removeEventListener('touchmove', this.boundHandlers.touchMove); document.removeEventListener('touchend', this.boundHandlers.touchEnd); document.removeEventListener('touchcancel', this.boundHandlers.touchEnd); document.removeEventListener('visibilitychange', this.boundHandlers.visibilityHandler); this.stopManualAnimation(); this.activeAnimations.clear(); console.log(`[GelpiGallery] Istanza #${this.instanceId} distrutta`); } } // ===== INIZIALIZZAZIONE ===== function initializeGalleries() { if (window.GelpiGallery.initialized) { console.warn('[GelpiGallery] Già inizializzato, skip'); return; } const galleries = document.querySelectorAll('.scrolling-gallery'); if (galleries.length === 0) { console.warn('[GelpiGallery] Nessuna gallery trovata'); return; } galleries.forEach((g, i) => { window.GelpiGallery.instances.push(new ScrollingGallery(g, i + 1)); }); window.GelpiGallery.initialized = true; console.log(`[GelpiGallery] ${galleries.length} gallery inizializzate`); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeGalleries); } else { initializeGalleries(); } })();
.scrolling-logos-container { width: 100%; overflow: hidden; position: relative; background: #000; display: flex; align-items: center; } .scrolling-logos-wrapper { display: flex; align-items: center; will-change: transform; } .scrolling-logos-wrapper img { display: block; height: auto; flex-shrink: 0; user-select: none; -webkit-user-drag: none; pointer-events: none; } @keyframes scroll-left { from { transform: translateX(0); } to { transform: translateX(-50%); } } @keyframes scroll-right { from { transform: translateX(-50%); } to { transform: translateX(0); } } .scroll-left { animation: scroll-left var(--scroll-speed) linear infinite; } .scroll-right { animation: scroll-right var(--scroll-speed) linear infinite; }
(function() { 'use strict'; // ======================================== // ⚙️ PARAMETRI DI CONFIGURAZIONE // ======================================== const CONFIG = { imageUrl: 'https://albertogelpi.com/wp-content/uploads/2025/10/clients_01.jpg', direction: 'right', opacity: 1, speed: 400, imageHeight: 50 }; // ======================================== // TROVA IL PROPRIO CONTAINER (multi-instance safe) // ======================================== const currentScript = document.currentScript; if (!currentScript) return; const container = currentScript.previousElementSibling; if (!container || !container.classList.contains('scrolling-logos-container')) return; const wrapper = container.querySelector('.scrolling-logos-wrapper'); if (!wrapper) return; // ======================================== // VARIABILI // ======================================== let imageCache = null; let isLoading = false; let resizeTimer = null; // ======================================== // RENDER // ======================================== function render(imgData) { const scale = CONFIG.imageHeight / imgData.naturalHeight; const scaledWidth = Math.round(imgData.naturalWidth * scale); container.style.height = CONFIG.imageHeight + 'px'; const containerWidth = container.offsetWidth || window.innerWidth || 1200; // Quante immagini servono per coprire il container (un "set") // Il wrapper contiene 2 set identici → translateX(-50%) = loop seamless const perSet = Math.max(Math.ceil(containerWidth / scaledWidth) + 1, 2); const totalImgs = perSet * 2; wrapper.innerHTML = ''; const fragment = document.createDocumentFragment(); for (let i = 0; i { isLoading = false; }, 3000); testImg.onload = () => { clearTimeout(timeout); imageCache = { naturalWidth: testImg.naturalWidth, naturalHeight: testImg.naturalHeight }; render(imageCache); isLoading = false; }; testImg.onerror = () => { clearTimeout(timeout); isLoading = false; }; testImg.src = CONFIG.imageUrl; } // ======================================== // RESIZE — ResizeObserver (più preciso di window resize) // ======================================== if (typeof ResizeObserver !== 'undefined') { const ro = new ResizeObserver(() => { if (!imageCache) return; clearTimeout(resizeTimer); resizeTimer = setTimeout(() => render(imageCache), 300); }); ro.observe(container); } else { // Fallback per browser vecchi window.addEventListener('resize', function() { if (!imageCache) return; clearTimeout(resizeTimer); resizeTimer = setTimeout(() => render(imageCache), 500); }, { passive: true }); } // ======================================== // START // ======================================== if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', loadAndRender); } else { loadAndRender(); } })();
.scrolling-logos-container { width: 100%; overflow: hidden; position: relative; background: #000; display: flex; align-items: center; } .scrolling-logos-wrapper { display: flex; align-items: center; will-change: transform; } .scrolling-logos-wrapper img { display: block; height: auto; flex-shrink: 0; user-select: none; -webkit-user-drag: none; pointer-events: none; } @keyframes scroll-left { from { transform: translateX(0); } to { transform: translateX(-50%); } } @keyframes scroll-right { from { transform: translateX(-50%); } to { transform: translateX(0); } } .scroll-left { animation: scroll-left var(--scroll-speed) linear infinite; } .scroll-right { animation: scroll-right var(--scroll-speed) linear infinite; }
(function() { 'use strict'; // ======================================== // ⚙️ PARAMETRI DI CONFIGURAZIONE // ======================================== const CONFIG = { imageUrl: 'https://albertogelpi.com/wp-content/uploads/2025/10/clients_02.jpg', direction: 'right', opacity: 1, speed: 400, imageHeight: 50 }; // ======================================== // TROVA IL PROPRIO CONTAINER (multi-instance safe) // ======================================== const currentScript = document.currentScript; if (!currentScript) return; const container = currentScript.previousElementSibling; if (!container || !container.classList.contains('scrolling-logos-container')) return; const wrapper = container.querySelector('.scrolling-logos-wrapper'); if (!wrapper) return; // ======================================== // VARIABILI // ======================================== let imageCache = null; let isLoading = false; let resizeTimer = null; // ======================================== // RENDER // ======================================== function render(imgData) { const scale = CONFIG.imageHeight / imgData.naturalHeight; const scaledWidth = Math.round(imgData.naturalWidth * scale); container.style.height = CONFIG.imageHeight + 'px'; const containerWidth = container.offsetWidth || window.innerWidth || 1200; const perSet = Math.max(Math.ceil(containerWidth / scaledWidth) + 1, 2); const totalImgs = perSet * 2; wrapper.innerHTML = ''; const fragment = document.createDocumentFragment(); for (let i = 0; i { isLoading = false; }, 3000); testImg.onload = () => { clearTimeout(timeout); imageCache = { naturalWidth: testImg.naturalWidth, naturalHeight: testImg.naturalHeight }; render(imageCache); isLoading = false; }; testImg.onerror = () => { clearTimeout(timeout); isLoading = false; }; testImg.src = CONFIG.imageUrl; } // ======================================== // RESIZE — ResizeObserver // ======================================== if (typeof ResizeObserver !== 'undefined') { const ro = new ResizeObserver(() => { if (!imageCache) return; clearTimeout(resizeTimer); resizeTimer = setTimeout(() => render(imageCache), 300); }); ro.observe(container); } else { window.addEventListener('resize', function() { if (!imageCache) return; clearTimeout(resizeTimer); resizeTimer = setTimeout(() => render(imageCache), 500); }, { passive: true }); } // ======================================== // START // ======================================== if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', loadAndRender); } else { loadAndRender(); } })();