Mode clair / sombre et thèmes de saison

This commit is contained in:
Yasser Boulayoune 2026-10-08 16:26:00 +02:00
parent da05adede1
commit 17cf2c8380
19 changed files with 3563 additions and 0 deletions

258
assets/js/effects.js vendored Normal file
View file

@ -0,0 +1,258 @@
/* effects.js : Scroll inertiel, nav auto-masquée, progression, rail, reveals,
scroll horizontal + skew, blob, wipe de page, menu mobile. Vanilla JS.
Base : handoff/theme-assets/effects.js (design v3), adapté au site :
- animations inconditionnelles (choix de Yasser) : seul ?ybstatic=1
(html.yb-static, posé dans header.php) les coupe, pour les captures ;
- le wipe couvre tous les liens internes, pas seulement [data-wipe] ;
- barres de compétences [data-bar], compteur des travaux = nb réel de cartes. */
const YB_STATIC = document.documentElement.classList.contains('yb-static');
const YB_TOUCH = matchMedia('(pointer: coarse)').matches;
/* Scroll inertiel (façon Lenis) : desktop uniquement ; sur mobile on garde le scroll natif. */
window.YBSmooth = { v: 0, init() {
if (YB_STATIC || YB_TOUCH) return;
let cur = scrollY, tgt = scrollY, wt = 0;
addEventListener('wheel', e => {
if (e.ctrlKey || document.body.classList.contains('no-scroll')) return;
e.preventDefault();
const max = document.documentElement.scrollHeight - innerHeight;
tgt = Math.max(0, Math.min(max, tgt + e.deltaY)); wt = performance.now();
}, { passive: false });
const tick = () => {
if (performance.now() - wt < 1600) { cur += (tgt - cur) * 0.095; scrollTo(0, cur); }
else { cur = tgt = scrollY; }
this.v += ((tgt - cur) - this.v) * 0.12; // vélocité, utilisée pour le skew
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
}};
/* Nav qui se masque en descendant + barre de progression lime en haut */
window.YBNav = { init(navSel, progSel) {
const nav = document.querySelector(navSel), prog = document.querySelector(progSel);
let last = scrollY;
const tick = () => {
const y = scrollY, max = Math.max(1, document.documentElement.scrollHeight - innerHeight);
if (prog) prog.style.transform = 'scaleX(' + Math.min(1, y / max) + ')';
if (nav && !document.body.classList.contains('no-scroll')) {
if (y - last > 1.5 && y > 180) nav.style.transform = 'translateY(-104%)';
else if (y - last < -1.5 || y <= 180) nav.style.transform = 'translateY(0)';
}
last = y; requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
}};
/* Rail vertical : chaque item [data-rail-target="#section"] s'allume quand sa section passe à mi-écran */
window.YBRail = { init(railSel) {
const rail = document.querySelector(railSel); if (!rail) return;
const items = [...rail.querySelectorAll('[data-rail-target]')];
const tick = () => {
let act = 0;
items.forEach((it, i) => { const s = document.querySelector(it.dataset.railTarget); if (s && s.getBoundingClientRect().top <= innerHeight * 0.5) act = i; });
items.forEach((it, i) => it.classList.toggle('is-active', i === act));
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
}};
/* Reveals au scroll : [data-reveal] ; les barres [data-bar] se remplissent à leur entrée */
window.YBReveals = { init() {
const all = document.querySelectorAll('[data-reveal], [data-bar]');
if (YB_STATIC) { all.forEach(el => el.classList.add('is-in')); return; }
const io = new IntersectionObserver(es => es.forEach(en => {
if (en.isIntersecting) { en.target.classList.add('is-in'); io.unobserve(en.target); }
}), { threshold: 0.12 });
all.forEach(el => io.observe(el));
}};
/* Scroll horizontal des travaux (desktop/tablette) + skew selon la vélocité + compteur 01/06.
Mobile (<720px) : CSS bascule la piste en scroll-snap horizontal natif (voir effects.css). */
window.YBHScroll = { init(secSel, trackSel, countSel) {
const sec = document.querySelector(secSel), track = document.querySelector(trackSel), count = document.querySelector(countSel);
if (!sec || !track || YB_STATIC) return;
const total = Math.max(1, track.querySelectorAll('.card').length);
const pad = n => String(n).padStart(2, '0');
const tick = () => {
if (innerWidth >= 720) {
const r = sec.getBoundingClientRect();
const prog = Math.max(0, Math.min(1, -r.top / (r.height - innerHeight)));
const dist = Math.max(0, track.scrollWidth - innerWidth);
const skew = Math.max(-4.5, Math.min(4.5, (YBSmooth.v || 0) * 0.011));
track.style.transform = 'translateX(' + (-prog * dist) + 'px) skewX(' + (-skew) + 'deg)';
if (count) count.textContent = pad(Math.min(total, 1 + Math.floor(prog * total))) + ' / ' + pad(total);
} else track.style.transform = '';
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
}};
/* Blob lumineux : suit la souris dans la zone, fondu + rétrécit quand elle sort */
window.YBBlob = { init(zoneSel, blobSel) {
const zone = document.querySelector(zoneSel), blob = document.querySelector(blobSel);
if (!zone || !blob || YB_TOUCH || YB_STATIC) { if (blob) blob.style.display = 'none'; return; }
const m = { x: 0, y: 0, bx: 0, by: 0, bo: 0 };
addEventListener('mousemove', e => { m.x = e.clientX; m.y = e.clientY; });
const tick = () => {
const r = zone.getBoundingClientRect();
m.bx += (m.x - r.left - m.bx) * 0.06; m.by += (m.y - r.top - m.by) * 0.06;
const inside = m.x >= r.left && m.x <= r.right && m.y >= r.top - 60 && m.y <= r.bottom + 60;
m.bo += ((inside ? 1 : 0) - m.bo) * 0.08;
blob.style.transform = 'translate(' + m.bx + 'px,' + m.by + 'px) scale(' + (0.6 + m.bo * 0.4) + ')';
blob.style.opacity = m.bo.toFixed(3);
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
}};
/* Hero : légère parallaxe du bloc titre à la souris (desktop) */
window.YBHeroParallax = { init(sel) {
const el = document.querySelector(sel);
if (!el || YB_TOUCH || YB_STATIC) return;
const m = { x: innerWidth / 2, y: innerHeight / 2, cx: innerWidth / 2, cy: innerHeight / 2 };
addEventListener('mousemove', e => { m.x = e.clientX; m.y = e.clientY; });
const tick = () => {
m.cx += (m.x - m.cx) * 0.25; m.cy += (m.y - m.cy) * 0.25;
if (el.getBoundingClientRect().bottom > 0) el.style.transform = 'translate(' + (m.cx - innerWidth / 2) * -0.012 + 'px,' + (m.cy - innerHeight / 2) * -0.008 + 'px)';
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
}};
/* Wipe de page : rideau lime. Le rideau (.yb-wipe) est rendu par header.php et
couvre l'écran dès le premier paint (html.is-entering) ; il remonte une fois la page prête.
Toute navigation interne le fait redescendre ; le libellé vient de data-wipe et
survit au changement de page via sessionStorage (clé ybWipe). */
/* Couleur des barres du navigateur (meta theme-color) : fond du thème, accent pendant le rideau */
window.YBBar = (function () {
let meta, wiping = false;
const css = v => getComputedStyle(document.documentElement).getPropertyValue(v).trim();
const set = c => { if (meta && c && meta.content !== c) meta.content = c; };
return {
accent() { wiping = true; set(css('--lime')); },
base() { wiping = false; set(css('--bg')); },
init() {
meta = document.querySelector('meta[name="theme-color"]');
if (!meta) { meta = document.createElement('meta'); meta.name = 'theme-color'; document.head.appendChild(meta); }
addEventListener('yb:theme', () => setTimeout(() => wiping ? this.accent() : this.base(), 0));
}
};
})();
window.YBWipe = { init() {
let w = document.querySelector('.yb-wipe');
if (!w) {
w = document.createElement('div');
w.className = 'yb-wipe';
w.innerHTML = '<span class="yb-wipe__label"></span>';
document.body.appendChild(w);
}
const label = w.querySelector('.yb-wipe__label');
try { sessionStorage.removeItem('ybWipe'); } catch (e) {}
if (!YB_STATIC) { YBBar.accent(); setTimeout(() => YBBar.base(), 520); } else YBBar.base();
requestAnimationFrame(() => requestAnimationFrame(() => document.documentElement.classList.remove('is-entering')));
if (YB_STATIC) return;
document.addEventListener('click', e => {
const a = e.target.closest('a[href]');
if (!a || a.target === '_blank' || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
const href = a.getAttribute('href');
if (!href || href.charAt(0) === '#' || /^(mailto:|tel:|javascript:)/i.test(href)) return;
if (a.host !== location.host) return;
if (/\/wp-(admin|login)/.test(a.pathname) || /\.(pdf|zip|png|jpe?g|webp|svg|gif)$/i.test(a.pathname)) return;
if (a.pathname === location.pathname && a.hash) return;
e.preventDefault();
const txt = a.dataset.wipe || '';
try { sessionStorage.setItem('ybWipe', txt); } catch (err) {}
label.textContent = txt;
w.classList.add('is-covering'); YBBar.accent();
setTimeout(() => { location.href = a.href; }, 480);
});
/* Retour arrière depuis le cache (bfcache) : ne pas rester bloqué sous le rideau */
addEventListener('pageshow', e => { if (e.persisted) { w.classList.remove('is-covering'); YBBar.base(); } });
}};
/* Coordonnées protégées contre la collecte automatique : [data-pv] (texte), [data-pv-href] (lien),
[data-pv-copy] (bouton copier). Encodage côté PHP : ROT13 → inversion → base64.
Décodage au premier geste humain (souris, toucher, clavier, focus, scroll). */
window.YBProtect = (function () {
let done = false;
const rot13 = t => t.replace(/[a-z]/gi, c => { const b = c <= 'Z' ? 65 : 97; return String.fromCharCode((c.charCodeAt(0) - b + 13) % 26 + b); });
const dec = v => { try { return rot13(decodeURIComponent(escape(atob(v))).split('').reverse().join('')); } catch (e) { return ''; } };
function reveal() {
if (done) return; done = true;
document.querySelectorAll('[data-pv]').forEach(el => { el.textContent = dec(el.dataset.pv); el.removeAttribute('data-pv'); });
document.querySelectorAll('[data-pv-href]').forEach(el => { el.setAttribute('href', dec(el.dataset.pvHref)); el.removeAttribute('data-pv-href'); });
document.querySelectorAll('[data-pv-copy]').forEach(el => { el.dataset.copy = dec(el.dataset.pvCopy); el.removeAttribute('data-pv-copy'); });
EVENTS.forEach(ev => removeEventListener(ev, reveal, true));
}
const EVENTS = ['pointermove', 'pointerdown', 'touchstart', 'keydown', 'focusin', 'wheel', 'scroll'];
return { reveal, init() {
if (!document.querySelector('[data-pv],[data-pv-href],[data-pv-copy]')) return;
if (YB_STATIC) return reveal();
EVENTS.forEach(ev => addEventListener(ev, reveal, { capture: true, passive: true }));
/* Un clic sur un lien protégé avant décodage : on décode puis on relance le clic */
document.addEventListener('click', e => {
const a = e.target.closest('[data-pv-href]');
if (!a) return;
e.preventDefault(); reveal(); a.click();
}, true);
} };
})();
/* Thème clair / sombre : bouton [data-theme-toggle], choix mémorisé (localStorage yb-theme).
Bascule animée : un cercle s'étend depuis le bouton (View Transitions), fondu sinon. */
window.YBTheme = { init() {
const btn = document.querySelector('[data-theme-toggle]'); if (!btn) return;
const root = document.documentElement;
const cur = () => root.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
const label = () => btn.setAttribute('aria-label', cur() === 'light' ? 'Passer en mode sombre' : 'Passer en mode clair');
const apply = t => {
root.setAttribute('data-theme', t);
try { localStorage.setItem('yb-theme', t); } catch (e) {}
label();
dispatchEvent(new CustomEvent('yb:theme', { detail: t }));
};
label();
btn.addEventListener('click', () => {
const next = cur() === 'light' ? 'dark' : 'light';
const r = btn.getBoundingClientRect(), x = r.left + r.width / 2, y = r.top + r.height / 2;
if (!document.startViewTransition || YB_STATIC) {
root.classList.add('theme-fade'); apply(next);
setTimeout(() => root.classList.remove('theme-fade'), 500);
return;
}
const end = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));
const vt = document.startViewTransition(() => apply(next));
vt.ready.then(() => root.animate(
{ clipPath: ['circle(0px at ' + x + 'px ' + y + 'px)', 'circle(' + end + 'px at ' + x + 'px ' + y + 'px)'] },
{ duration: 750, easing: 'cubic-bezier(.7,0,.3,1)', pseudoElement: '::view-transition-new(root)' }
));
});
}};
/* Menu mobile plein écran : bouton [data-menu-toggle], panneau [data-menu] */
window.YBMenu = { init() {
const btn = document.querySelector('[data-menu-toggle]'), panel = document.querySelector('[data-menu]');
if (!btn || !panel) return;
const set = open => {
panel.classList.toggle('is-open', open);
document.body.classList.toggle('no-scroll', open);
btn.textContent = open ? 'FERMER ✕' : 'MENU';
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
panel.setAttribute('aria-hidden', open ? 'false' : 'true');
};
btn.addEventListener('click', () => set(!panel.classList.contains('is-open')));
panel.querySelectorAll('a').forEach(a => a.addEventListener('click', () => set(false)));
addEventListener('keydown', e => { if (e.key === 'Escape' && panel.classList.contains('is-open')) set(false); });
}};
document.addEventListener('DOMContentLoaded', () => {
YBBar.init(); YBProtect.init(); YBTheme.init(); YBSmooth.init(); YBReveals.init(); YBWipe.init(); YBMenu.init();
YBNav.init('.site-nav', '.scroll-progress');
YBRail.init('.section-rail');
YBHScroll.init('#travaux', '.works-track', '.works-count');
YBBlob.init('#apropos', '.about-blob');
YBHeroParallax.init('.hero__content');
if (window.YBCursor) YBCursor.init();
if (window.YBOrb) YBOrb.init('#orb-canvas', { sections: { works: '#travaux', about: '#apropos', cta: '#cta' } });
});