yb2026/assets/js/cursor.js

106 lines
8.3 KiB
JavaScript

/* cursor.js : Curseur custom (desktop uniquement)
YBCursor.init();
Au repos : point accent. Sur un lien / bouton : le point s'étire en index et la main apparaît autour
(le bout du doigt reste sur la position de la souris). data-cursor-label="VOIR LE PROJET" = label contextuel.
La main change selon la fête quand c'est cohérent (html[data-season]), relue à chaque événement yb:theme.
Dans la roue des projets et articles (.cp-wheel) : le point devient une loupe, elle aussi accordée à la fête. */
window.YBCursor = (function () {
let wrap, dot, hand, loupe, lbl, curSeason = null, override = null;
const m = { x: -100, y: -100, cx: -100, cy: -100 };
/* Silhouette commune : main qui pointe, bout de l'index en (11.5, 1) */
const SHAPE = 'M11.5 1C13.2 1 14.5 2.3 14.5 4v9.2c.5-.7 1.4-1.1 2.4-1.1 1.3 0 2.4.8 2.7 1.9.5-.5 1.3-.8 2.2-.8 1.5 0 2.7 1.1 2.8 2.5.5-.4 1.2-.6 1.9-.6 1.5 0 2.7 1.2 2.7 2.7V25c0 5.5-4 9.5-9.5 9.5h-2.3c-3.4 0-5.8-1.3-7.8-3.9l-5.4-7.1c-.9-1.2-.7-2.9.5-3.8 1.2-.9 2.8-.7 3.7.4l.1.1V4c0-1.7 1.3-3 3-3z';
const CREASES = 'M14.5 13.2V19M19.6 14v5.5M24.6 15.7V20';
const base = (fill, stroke, extra, crease) =>
'<path d="' + SHAPE + '" fill="' + fill + '" stroke="' + stroke + '" stroke-width="1.4" stroke-linejoin="round"/>' +
'<path d="' + CREASES + '" fill="none" stroke="' + (crease || stroke) + '" stroke-width="1.2" stroke-linecap="round"/>' + (extra || '');
const HANDS = {
'': () => base('var(--lime)', 'var(--bg)'),
// gant squelette : os blancs sur gant noir
halloween: () => base('#15101a', 'var(--lime)',
'<g fill="none" stroke="#efe6d2" stroke-width="1.6" stroke-linecap="round"><path d="M11.5 3.6v3.4M11.5 9.2v3.8M11.5 15.2v4"/><path d="M17 15.6v2.6M22 16.6v2.4M27 18.4v2.2"/><path d="M12.6 23.8l2.8 5.6M17 22.6l1 6.6M21.4 22.6l-.6 6.6M25.6 23.6l-2.4 5.8"/><circle cx="18.6" cy="31.2" r="1.2" fill="#efe6d2"/></g>', '#efe6d2'),
// gant rouge et revers en fourrure blanche
noel: () => base('#d61f35', '#5a0b16',
'<rect x="9.2" y="29.6" width="20.6" height="6.2" rx="3.1" fill="#fdfbf6" stroke="#5a0b16" stroke-width="1.1"/>', '#8e1222'),
// gant vert, trèfle doré sur le dos de la main
'st-patrick': () => base('#1b8f4c', '#0b3d21',
'<g fill="#f2c14e" transform="translate(21 25)"><circle cx="-1.5" cy="-1.6" r="1.7"/><circle cx="1.5" cy="-1.6" r="1.7"/><circle cx="0" cy="1" r="1.7"/></g>'),
// gant blanc de cérémonie, revers doré, étincelle au bout du doigt
'nouvel-an': () => base('#ffffff', '#16141c',
'<rect x="9.4" y="30" width="20.2" height="5.4" rx="2.7" fill="#f2c14e" stroke="#16141c" stroke-width="1.1"/><path class="yb-hand-spark" d="M4 2l.9 2.1L7 5l-2.1.9L4 8l-.9-2.1L1 5l2.1-.9z" fill="#f2c14e"/>', '#9aa0b0'),
// gant de fête : turquoise, petite bague dorée
anniversaire: () => base('var(--lime)', 'var(--bg)',
'<path d="M8.6 15.4h5.8" stroke="#ffcf4a" stroke-width="2" stroke-linecap="round"/>')
};
/* Loupe : centre de la lentille en (14, 14) = position de la souris ; manche dessiné à plat puis tourné à 45° */
const lens = (ring, glass, handle, extra, inside) =>
'<g transform="rotate(45 14 14)"><rect x="24" y="11.6" width="14" height="4.8" rx="2.4" fill="' + handle + '" stroke="var(--bg)" stroke-width="1"/>' + (extra || '') + '</g>' +
'<circle cx="14" cy="14" r="10" fill="' + glass + '"/>' + (inside || '') +
'<circle cx="14" cy="14" r="10" fill="none" stroke="' + ring + '" stroke-width="3"/>' +
'<path d="M8.6 11.2a6 6 0 0 1 4-3.6" fill="none" stroke="#fff" stroke-opacity=".7" stroke-width="1.6" stroke-linecap="round"/>';
const stripes = (cols, w) => cols.map((c, i) => '<rect x="' + (24 + i * w) + '" y="11.6" width="' + w + '" height="4.8" fill="' + c + '"/>').join('');
const LOUPES = {
'': () => lens('var(--lime)', 'rgba(255,255,255,.08)', 'var(--ink)'),
// toile d'araignée dans le verre, manche en bois sombre
halloween: () => lens('var(--lime)', 'rgba(255,123,28,.1)', '#9a6a3a', '',
'<path d="M14 4.5v19M4.5 14h19M7.3 7.3l13.4 13.4M20.7 7.3 7.3 20.7M9.5 14a4.5 4.5 0 0 1 9 0 4.5 4.5 0 0 1-9 0M6.8 14a7.2 7.2 0 0 1 14.4 0 7.2 7.2 0 0 1-14.4 0" fill="none" stroke="#efe6d2" stroke-opacity=".35" stroke-width=".6"/>'),
// manche en sucre d'orge, cerclage doré
noel: () => lens('#f2c14e', 'rgba(255,255,255,.1)', '#fdfbf6', stripes(['#d61f35', 'transparent', '#d61f35', 'transparent', '#d61f35', 'transparent', '#d61f35'], 2)),
'st-patrick': () => lens('#f2c14e', 'rgba(61,220,132,.12)', '#1b8f4c', '', '<g fill="#3ddc84" fill-opacity=".55" transform="translate(17.5 17.5) scale(.8)"><circle cx="-1.5" cy="-1.6" r="1.7"/><circle cx="1.5" cy="-1.6" r="1.7"/><circle cx="0" cy="1" r="1.7"/></g>'),
'nouvel-an': () => lens('#f2c14e', 'rgba(242,193,78,.1)', '#d9dde8', stripes(['#f2c14e', '#f2c14e'], 2), '<path class="yb-hand-spark" d="M18 7l.7 1.6 1.6.7-1.6.7-.7 1.6-.7-1.6-1.6-.7 1.6-.7z" fill="#fff"/>'),
juillet: () => lens('#ffffff', 'rgba(255,255,255,.08)', '#ffffff', stripes(['#1f3fa8', '#1f3fa8', '#ffffff', '#ffffff', '#c8102e', '#c8102e', '#c8102e'], 2)),
'black-friday': () => lens('#ffd400', 'rgba(255,212,0,.1)', '#0b0b0b', stripes(['#ffd400', '#0b0b0b', '#ffd400', '#0b0b0b', '#ffd400', '#0b0b0b', '#ffd400'], 2)),
anniversaire: () => lens('var(--lime)', 'rgba(46,230,197,.1)', '#ffffff', stripes(['#2ee6c5', '#ffffff', '#ff6b81', '#ffffff', '#ffcf4a', '#ffffff', '#2ee6c5'], 2)),
poisson: () => lens('#4fd1ff', 'rgba(79,209,255,.12)', '#ffb347', '', '<circle cx="11" cy="17" r="1.6" fill="none" stroke="#fff" stroke-opacity=".5" stroke-width=".7"/><circle cx="16.5" cy="10.5" r="1" fill="none" stroke="#fff" stroke-opacity=".5" stroke-width=".7"/>')
};
function paint() {
const s = override !== null ? override : document.documentElement.getAttribute('data-season') || '';
const k = HANDS[s] ? s : '';
// aperçu (roue des thèmes) : la main prend aussi l'accent de la fête survolée
const acc = override !== null && override ? (((window.YB_SEASONS || {}).list || {})[override] || {}).accent || '' : '';
[hand, loupe].forEach(e => acc ? e.style.setProperty('--lime', acc) : e.style.removeProperty('--lime'));
const key = s + '|' + k;
if (key === curSeason) return;
curSeason = key;
hand.innerHTML = '<svg viewBox="-1 -1 34 38" width="34" height="38">' + HANDS[k]() + '</svg>';
loupe.innerHTML = '<svg viewBox="0 0 34 34" width="34" height="34">' + (LOUPES[s] || LOUPES[''])() + '</svg>';
}
function setState(label, link, inWheel) {
lbl.textContent = label || '';
lbl.style.opacity = label ? '1' : '0';
lbl.style.transform = label ? 'scale(1)' : 'scale(.6)';
wrap.classList.toggle('is-loupe', !!inWheel);
wrap.classList.toggle('is-hand', !!link && !inWheel);
}
function loop() {
m.cx += (m.x - m.cx) * 0.25; m.cy += (m.y - m.cy) * 0.25;
wrap.style.transform = 'translate(' + m.cx + 'px,' + m.cy + 'px)';
requestAnimationFrame(loop);
}
return {
/* Montre la main d'une fête sans l'activer ('' = main normale, null = revenir à la fête affichée) */
preview(key) { if (!hand) return; override = key; curSeason = null; paint(); },
init() {
// Animations inconditionnelles : seul ?ybstatic=1 (html.yb-static) coupe le curseur
if (matchMedia('(pointer: coarse)').matches || document.documentElement.classList.contains('yb-static')) return;
document.documentElement.classList.add('has-custom-cursor'); // CSS : .has-custom-cursor, .has-custom-cursor * { cursor:none }
wrap = document.createElement('div'); wrap.className = 'yb-cursor';
dot = document.createElement('div'); dot.className = 'yb-cursor__dot';
hand = document.createElement('div'); hand.className = 'yb-cursor__hand';
loupe = document.createElement('div'); loupe.className = 'yb-cursor__loupe';
lbl = document.createElement('div'); lbl.className = 'yb-cursor__label';
wrap.append(dot, hand, loupe, lbl); document.body.appendChild(wrap);
paint(); addEventListener('yb:theme', paint);
addEventListener('mousemove', e => { m.x = e.clientX; m.y = e.clientY; });
document.addEventListener('mouseover', e => {
const w = e.target.closest('.cp-wheel');
const t = e.target.closest('[data-cursor-label], a, button, [data-cursor]');
if (!t) return setState('', false, w);
setState(t.getAttribute('data-cursor-label') || '', true, w);
});
requestAnimationFrame(loop);
}
};
})();