scroll sync on projet et evenement page bug fix
This commit is contained in:
@@ -43,6 +43,10 @@ gsap.registerPlugin(ScrollTrigger);
|
||||
const gsap_mm = gsap.matchMedia();
|
||||
console.log('gsap_mm', gsap_mm);
|
||||
|
||||
// Instance Lenis partagée : initPageFull en a besoin pour resynchroniser la
|
||||
// plage de scroll après avoir gonflé le spacer (voir ensureScrollForTitle).
|
||||
let lenis = null;
|
||||
|
||||
// ___ _ _
|
||||
// |_ _|_ _ (_) |_
|
||||
// | || ' \| | _|
|
||||
@@ -88,7 +92,7 @@ gsap.registerPlugin(ScrollTrigger);
|
||||
const content = scroller && scroller.querySelector('.layout-content');
|
||||
if (!scroller || !content) return;
|
||||
|
||||
const lenis = new Lenis({
|
||||
lenis = new Lenis({
|
||||
wrapper: scroller, // conteneur scrollable custom
|
||||
content: content, // contenu réel (le 1er enfant est l'ancre skip-link)
|
||||
duration: 1.2,
|
||||
@@ -98,6 +102,13 @@ gsap.registerPlugin(ScrollTrigger);
|
||||
lenis.on('scroll', ScrollTrigger.update);
|
||||
gsap.ticker.add((time) => lenis.raf(time * 1000));
|
||||
gsap.ticker.lagSmoothing(0);
|
||||
|
||||
// Le recalcul post-load (lenis.resize + ScrollTrigger.refresh) est
|
||||
// centralisé dans initPageFull pour garantir l'ordre : gonflage du spacer
|
||||
// → lenis.resize → refresh. Le faire ici (avant le gonflage) donnerait à
|
||||
// Lenis une plage de scroll périmée sur les pages courtes (cf. node/586).
|
||||
|
||||
return lenis;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,9 +157,6 @@ gsap.registerPlugin(ScrollTrigger);
|
||||
// Nettoyage automatique des tweens/triggers quand on repasse sous 768px.
|
||||
return () => {};
|
||||
});
|
||||
|
||||
// Les images chargent après coup et changent la hauteur des colonnes.
|
||||
window.addEventListener('load', () => ScrollTrigger.refresh());
|
||||
}
|
||||
|
||||
function initPageFull(){
|
||||
@@ -157,86 +165,163 @@ gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
const headerScaleTarget = 0.4;
|
||||
|
||||
// PAGE TITLE SCALING DOWN
|
||||
const cols = document.querySelector('article.node-type-projet.view-mode-full .cols, article.node-type-evenement.view-mode-full .cols');
|
||||
const leftColumn = cols && cols.querySelector('.col.left');
|
||||
const rightColumn = cols && cols.querySelector('.col.right .field_images');
|
||||
const spacer = leftColumn && leftColumn.querySelector('.spacer');
|
||||
// Hauteur CSS de base du spacer (5em), mesurée AVANT tout gonflage JS.
|
||||
const spacerBaseHeight = spacer ? (parseFloat(getComputedStyle(spacer).height) || 0) : 0;
|
||||
|
||||
const pageHeader = document.querySelector('\
|
||||
article.node-type-projet.view-mode-full .cols>.col.left>header,\
|
||||
article.node-type-evenement.view-mode-full .cols>.col.left>header\
|
||||
');
|
||||
console.log('pageHeader', pageHeader);
|
||||
|
||||
const initHeaderH = pageHeader ? pageHeader.offsetHeight : 0;
|
||||
|
||||
if (pageHeader) {
|
||||
// needs requestAnimationFrame to get the real titleHeight after splitting
|
||||
|
||||
// console.log(`initHeaderH: ${initHeaderH}`);
|
||||
const end = initHeaderH - initHeaderH * headerScaleTarget;
|
||||
// console.log(`end: ${end}`);
|
||||
if (!pageHeader && !(cols && leftColumn)) return;
|
||||
|
||||
// Un seul contexte de timing pour TOUT le comportement de la page full
|
||||
// (scale-down du titre + gel de la colonne gauche + contexte sticky des
|
||||
// images). Le double rAF attend la hauteur réelle du titre après Splitting ;
|
||||
// layoutSync() dimensionne les colonnes AVANT la création des ScrollTriggers.
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
gsap_mm.add('(min-width: 768px)', () => {
|
||||
|
||||
gsap_mm.add('(min-width: 768px)', () => {
|
||||
gsap.to(pageHeader, {
|
||||
scale: headerScaleTarget,
|
||||
y: end,
|
||||
const viewport = () => scroller.clientHeight;
|
||||
|
||||
// Amplitude du scale-down du titre, en px de scroll.
|
||||
const titleEnd = () => pageHeader ? pageHeader.offsetHeight * (1 - headerScaleTarget) : 0;
|
||||
|
||||
// Portion du spacer ajoutée par le JS (hors hauteur CSS de base 5em).
|
||||
const spacerAdded = () => spacer
|
||||
? Math.max(0, (parseFloat(getComputedStyle(spacer).height) || 0) - spacerBaseHeight)
|
||||
: 0;
|
||||
// Hauteur du contenu RÉEL de la colonne gauche (sans le gonflage JS).
|
||||
const leftContentHeight = () => leftColumn ? leftColumn.offsetHeight - spacerAdded() : 0;
|
||||
|
||||
// Hauteur en flux de la pile d'images (les .field-item sont sticky,
|
||||
// donc toujours dans le flux) et hauteur de la dernière image.
|
||||
const imagesFlow = () => {
|
||||
if (!rightColumn) return 0;
|
||||
let sum = 0;
|
||||
rightColumn.querySelectorAll('.field-item').forEach(item => { sum += item.offsetHeight; });
|
||||
return sum;
|
||||
};
|
||||
const lastImageHeight = () => {
|
||||
if (!rightColumn) return 0;
|
||||
const last = rightColumn.querySelector('.field-item:last-child');
|
||||
return last ? last.offsetHeight : 0;
|
||||
};
|
||||
|
||||
// Spacer gauche : garantit SEULEMENT assez de scroll pour dérouler le
|
||||
// scale-down du titre quand le contenu est trop court (peu/pas d'image
|
||||
// + texte bref → node/586). Minimal, pour NE PAS sur-scroller (sinon en
|
||||
// fin de scroll tout est parti en haut → node/572).
|
||||
const ensureScrollForTitle = () => {
|
||||
if (!spacer || !pageHeader) return;
|
||||
const end = titleEnd();
|
||||
const scrollRange = scroller.scrollHeight - scroller.clientHeight;
|
||||
if (scrollRange < end) {
|
||||
const add = Math.max(0, end + viewport() - leftContentHeight());
|
||||
spacer.style.height = (spacerBaseHeight + add) + 'px';
|
||||
void scroller.offsetHeight;
|
||||
}
|
||||
};
|
||||
|
||||
// Contexte sticky de la colonne droite : le `:after` étend .field_images
|
||||
// sous la dernière image. Hauteur = MAX de deux besoins (aucun ne
|
||||
// référence scrollRange → pas de boucle de rétroaction) :
|
||||
// - `leftH - imagesFlow` : couvrir tout le scroll d'une gauche haute
|
||||
// (texte long) pour que la pile reste figée jusqu'au bout (node/573) ;
|
||||
// - `viewport - dernièreImage` : le « rab » de scroll pour que la
|
||||
// DERNIÈRE image atteigne le haut de la pile et y termine le scroll.
|
||||
// Sans lui, sur une gauche courte + images, le scroll s'arrête à
|
||||
// `imagesFlow - viewport` alors que la dernière image n'atteint le
|
||||
// haut qu'à `imagesFlow - dernièreImage` → elle n'y arrive jamais.
|
||||
const adjustStickyContext = () => {
|
||||
if (!rightColumn) return;
|
||||
const extent = Math.max(
|
||||
0,
|
||||
leftColumn.offsetHeight - imagesFlow(),
|
||||
viewport() - lastImageHeight()
|
||||
);
|
||||
rightColumn.style.setProperty('--sticky-extent', extent + 'px');
|
||||
};
|
||||
|
||||
// Dimensionne les colonnes (spacer gauche puis :after droit, dans cet
|
||||
// ordre car l'extent lit leftColumn.offsetHeight), puis resynchronise
|
||||
// Lenis sur la nouvelle plage de scroll.
|
||||
const layoutSync = () => {
|
||||
ensureScrollForTitle();
|
||||
adjustStickyContext();
|
||||
void scroller.offsetHeight; // force le reflow
|
||||
if (lenis) lenis.resize(); // Lenis voit la nouvelle hauteur
|
||||
};
|
||||
|
||||
// Seuil de scroll (px) où la colonne gauche se fige : MAX de
|
||||
// - le débordement du contenu réel (bas du texte au bas du viewport) ;
|
||||
// - titleEnd (fin du scale-down).
|
||||
// Le max sur titleEnd est essentiel : pendant que le titre rétrécit,
|
||||
// l'en-tête est épinglé en haut et le contenu remonte pour suivre son
|
||||
// bord bas ; un gel démarrant avant la fin du scale-down pousserait
|
||||
// l'en-tête vers le bas → « le titre diminue en descendant » (576, 584).
|
||||
const leftFreezeStart = () => Math.max(0, leftContentHeight() - viewport(), titleEnd());
|
||||
|
||||
// 1) Colonnes dimensionnées AVANT la création des ScrollTriggers.
|
||||
layoutSync();
|
||||
|
||||
// 2) SCALE-DOWN DU TITRE
|
||||
if (pageHeader) {
|
||||
gsap.to(pageHeader, {
|
||||
scale: headerScaleTarget,
|
||||
y: titleEnd, // compense la réduction pour garder le bord haut fixe
|
||||
ease: 'none',
|
||||
scrollTrigger: {
|
||||
scroller: scroller, start: 0, end: titleEnd, scrub: true, invalidateOnRefresh: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 3) GEL DE LA COLONNE GAUCHE
|
||||
// On NE pin PAS (le pin sort l'élément du flux et casse le flex .cols).
|
||||
// On translate : défilement natif jusqu'à `leftFreezeStart`, puis
|
||||
// contre-translation 1:1 du scroll → elle se fige en restant un
|
||||
// flex-item normal. `.cols` est en `align-items: flex-start`.
|
||||
if (cols && leftColumn) {
|
||||
gsap.fromTo(leftColumn,
|
||||
{ y: 0 },
|
||||
{
|
||||
y: () => (scroller.scrollHeight - scroller.clientHeight) - leftFreezeStart(),
|
||||
ease: 'none',
|
||||
scrollTrigger: {
|
||||
scroller: scroller,
|
||||
start: 0,
|
||||
end: end, // scroll maximum du conteneur (robuste au resize)
|
||||
scrub: true,
|
||||
invalidateOnRefresh: true,
|
||||
scroller: scroller, start: leftFreezeStart, end: 'max', scrub: true, invalidateOnRefresh: true,
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 4) Recalage quand les hauteurs changent (images/police) : dans
|
||||
// l'ordre layoutSync (→ lenis.resize) puis refresh des ScrollTriggers.
|
||||
const recalc = () => { layoutSync(); ScrollTrigger.refresh(); };
|
||||
const imgs = rightColumn ? Array.from(rightColumn.querySelectorAll('img')) : [];
|
||||
imgs.forEach(img => { if (!img.complete) img.addEventListener('load', recalc); });
|
||||
window.addEventListener('load', recalc);
|
||||
// La police Epilogue peut charger après `load` et changer la hauteur /
|
||||
// le nombre de lignes du titre → titleEnd faux (node/584 : titre coupé,
|
||||
// parfois sur 3 lignes). On recale quand les polices sont prêtes.
|
||||
if (document.fonts && document.fonts.ready) document.fonts.ready.then(recalc);
|
||||
|
||||
// 5) Nettoyage au repassage < 768px.
|
||||
return () => {
|
||||
imgs.forEach(img => img.removeEventListener('load', recalc));
|
||||
window.removeEventListener('load', recalc);
|
||||
if (spacer) spacer.style.height = '';
|
||||
if (rightColumn) rightColumn.style.removeProperty('--sticky-extent');
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// BLOCAGE DE LA COLONNE GAUCHE
|
||||
// La gauche défile normalement puis se fige (pin) quand le bas de son
|
||||
// contenu atteint le bas du viewport, pendant que la colonne droite (plus
|
||||
// haute) continue. Piloté en GSAP plutôt qu'en `position: sticky` (qui ne
|
||||
// prend pas dans ce contexte scroller custom + GSAP). `.cols` est en
|
||||
// `align-items: flex-start`, donc .col.left a bien la hauteur de son
|
||||
// contenu (sinon `bottom bottom` tomberait à la fin du scroll).
|
||||
const cols = document.querySelector('article.node-type-projet.view-mode-full .cols');
|
||||
const leftColumn = cols && cols.querySelector('.col.left');
|
||||
if (cols && leftColumn) {
|
||||
// Doit rester identique au scale-down du titre (voir plus haut).
|
||||
const titleScaleTarget = 0.4;
|
||||
// Seuil de scroll (px) après lequel la colonne gauche se fige :
|
||||
// - gauche plus haute que le viewport : quand son bas atteint le bas ;
|
||||
// - gauche plus courte : quand le scale-down du titre est terminé, pour
|
||||
// qu'elle remonte AVEC le titre puis s'arrête (sans disparaître en haut).
|
||||
const leftFreezeStart = () => {
|
||||
const overflow = leftColumn.offsetHeight - scroller.clientHeight;
|
||||
if (overflow > 0) return overflow;
|
||||
return pageHeader ? initHeaderH * (1 - headerScaleTarget) : 0;
|
||||
};
|
||||
|
||||
gsap_mm.add('(min-width: 768px)', () => {
|
||||
// On NE pin PAS (le pin sort l'élément du flux et casse le flex .cols).
|
||||
// On translate la colonne : défilement natif jusqu'à `leftFreezeStart`,
|
||||
// puis contre-translation 1:1 du scroll → elle se fige, tout en restant
|
||||
// un flex-item normal (la colonne droite garde sa place).
|
||||
gsap.fromTo(leftColumn,
|
||||
{ y: 0 },
|
||||
{
|
||||
y: () => (scroller.scrollHeight - scroller.clientHeight) - leftFreezeStart(),
|
||||
ease: 'none',
|
||||
scrollTrigger: {
|
||||
scroller: scroller,
|
||||
start: leftFreezeStart, // px de scroll où la gauche se fige
|
||||
end: 'max',
|
||||
scrub: true,
|
||||
invalidateOnRefresh: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user