Questo spazio è parte della mia
vita.
storie, progetti e il mio modo di raccontare.
Un lungo viaggio
fatto di tentativi, fatiche, cadute,
successi,
paure e risate.
Il mio obiettivo:
plasmare
emozioni.
In un film, un documentario o nei trenta secondi di uno spot.
LUNGOMETRAGGI
L'OMBRA DEL LUPO
2020 | thriller drama
Nico è un poliziotto violento, rientrato dopo anni nel suo paese per dare l’ultimo saluto alla madre Eleonora, ammalatasi all’improvviso. Negli stessi giorni alcuni cadaveri vengono ritrovati nel bosco, messi in mostra come in un macabro monito…
LA MIA SECONDA VOLTA
2019 | young adult
Diciott’anni, vitale e creativa, un talento ancora da indirizzare: questa è Giorgia. Brucia dalla voglia di finire la quinta liceo artistico ed evadere dalla provincia dove è cresciuta… la stessa in cui, per studiare all’Accademia di Belle Arti, s’è ritirata Ludovica...
DOCUMENTARI
BOTANICAL GARDENS',
description: 'Menton, Antibes, Hanbury:
un viaggio nel Pianeta Terra.',
link: 'https://albertogelpi.com/index.php/botanical-gardens/'
},
{
path: 'https://albertogelpi.com/wp-content/uploads/2026/02/doc_heritage4.jpg',
title: 'HERITAGE',
description: 'The flussi migratori italiani 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:
(stra)ordinarie storie di
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 uomini e animali
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');
});
AFFIANCO',
description: 'Lives in the Mirror:
(stra)ordinarie storie di
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 uomini e animali
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');
});
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; }
${image.title}
${image.description}
SPOT
/* ==== 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();
}
})();







