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
SHADOW OF THE WOLF
2020 | thriller drama
Nico is a policeman who has returned to his country after years to say goodbye to his mother, Eleonora, who suddenly fell ill. In the same days, some corpses are found in the woods, put on display as in a macabre warning.
MY SECOND CHANCE
2019 | young adult
Eighteen years old, full of life and creative, a talent still finding its direction: this is Giorgia. She can't wait to finish her final year at art school and escape from the small town where she grew up... the very same town where Ludovica moved to study at the Academy of Fine Arts.
documentaries
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');
});
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');
});
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}
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();
}
})();







