// Veille v2 — kinetic, dramatic hero with mega type const { useEffect, useRef } = React; function ScreensaverCanvas() { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); const dpr = Math.min(window.devicePixelRatio || 1, 2); let raf = 0, w = 0, h = 0; const PARTS = []; const COUNT = 90; const STREAMS = 6; function resize() { const r = canvas.getBoundingClientRect(); w = r.width; h = r.height; canvas.width = w * dpr; canvas.height = h * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } function init() { resize(); PARTS.length = 0; for (let i = 0; i < COUNT; i++) { PARTS.push({ x: Math.random() * w, y: Math.random() * h, vx: (Math.random() - 0.5) * 0.4, vy: (Math.random() - 0.5) * 0.4, r: Math.random() * 2.5 + 0.5, a: Math.random() * 0.6 + 0.2, hue: 200 + Math.random() * 30, }); } } function step() { ctx.clearRect(0, 0, w, h); const t = performance.now() * 0.0004; // Aurora streams for (let i = 0; i < STREAMS; i++) { const yBase = h * (0.15 + i * 0.13); ctx.beginPath(); const grad = ctx.createLinearGradient(0, 0, w, 0); grad.addColorStop(0, 'rgba(77,123,214,0)'); grad.addColorStop(0.5, `rgba(${i % 2 ? '180,200,240' : '120,160,230'},${0.18 + i * 0.02})`); grad.addColorStop(1, 'rgba(77,123,214,0)'); ctx.strokeStyle = grad; ctx.lineWidth = 1.5; ctx.moveTo(0, yBase); for (let x = 0; x <= w; x += 12) { const y = yBase + Math.sin((x * 0.004) + t * (i + 1) * 3 + i * 0.7) * (30 + i * 8); ctx.lineTo(x, y); } ctx.stroke(); } // Particles for (const p of PARTS) { p.x += p.vx; p.y += p.vy; if (p.x < -10) p.x = w + 10; else if (p.x > w + 10) p.x = -10; if (p.y < -10) p.y = h + 10; else if (p.y > h + 10) p.y = -10; const grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.r * 8); grad.addColorStop(0, `hsla(${p.hue}, 60%, 80%, ${p.a})`); grad.addColorStop(1, 'transparent'); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(p.x, p.y, p.r * 8, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = `rgba(255,255,255,${p.a + 0.3})`; ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill(); } // Connections for (let i = 0; i < PARTS.length; i++) { for (let j = i + 1; j < PARTS.length; j++) { const a = PARTS[i], b = PARTS[j]; const dx = a.x - b.x, dy = a.y - b.y; const d2 = dx * dx + dy * dy; if (d2 < 18000) { const alpha = (1 - d2 / 18000) * 0.22; ctx.strokeStyle = `rgba(120,160,230,${alpha})`; ctx.lineWidth = 0.6; ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke(); } } } raf = requestAnimationFrame(step); } init(); step(); window.addEventListener('resize', init); return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', init); }; }, []); return ; } // Arrondi à la dizaine inférieure : 169 → 160, 170 → 170 function floorTen(n) { return Math.floor(n / 10) * 10; } // Textes par défaut (fallback si settings absent du JSON) const VEILLE_DEFAULTS = { eyebrow: "La Vallée de l'aluminium", titre: ["Découvrez le", "circuit de", "l'aluminium"], sous_titre: "Dix étapes, des centaines d'entreprises, une filière entière ancrée dans la région.", cta: "Touchez l'écran pour commencer", stats: { etapes: "Étapes du circuit", entreprises: "Entreprises", services: "Services référencés", ventes: "emplois dans la région", emplois_nombre: "37", emplois_suffixe: "k+", }, }; function Screensaver({ data, onWake }) { // Lecture des settings depuis le JSON exporté par le CMS (si disponibles) const cfg = React.useMemo(() => { const s = data?.settings?.veille || {}; return { eyebrow: s.eyebrow ?? VEILLE_DEFAULTS.eyebrow, titre: s.titre ?? VEILLE_DEFAULTS.titre, sous_titre: s.sous_titre ?? VEILLE_DEFAULTS.sous_titre, cta: s.cta ?? VEILLE_DEFAULTS.cta, stats: { ...VEILLE_DEFAULTS.stats, ...(s.stats || {}) }, emplois_nombre: s.stats?.emplois_nombre ?? VEILLE_DEFAULTS.stats.emplois_nombre, emplois_suffixe: s.stats?.emplois_suffixe ?? VEILLE_DEFAULTS.stats.emplois_suffixe, }; }, [data]); const stats = React.useMemo(() => { const ents = new Set(); let services = 0; data.etapes.forEach(et => et.categories.forEach(c => { c.services.forEach(s => { if (s.nom) services++; s.entreprises.forEach(e => ents.add(e.nom)); }); })); return { entreprises: ents.size, etapes: data.etapes.length, services }; }, [data]); // Animated counters on mount const [animated, setAnimated] = React.useState({ etapes: 0, ent: 0, srv: 0, pct: 0 }); useEffect(() => { const start = performance.now(); const dur = 1400; let raf = 0; const tick = (t) => { const p = Math.min(1, (t - start) / dur); const e = 1 - Math.pow(1 - p, 3); setAnimated({ etapes: Math.round(stats.etapes * e), ent: Math.round(floorTen(stats.entreprises) * e), srv: Math.round(stats.services * e), pct: Math.round(parseInt(cfg.emplois_nombre || 37) * e), }); if (p < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [stats.etapes, stats.entreprises, stats.services]); // Titre : 3 lignes — ligne 2 avec "circuit" en bleu, ligne 3 en italique const [tLigne1, tLigne2, tLigne3] = cfg.titre; return (
SVA
{cfg.eyebrow}

{tLigne1} {tLigne2} {tLigne3}

{cfg.sous_titre}

{cfg.cta}
{animated.etapes}
{cfg.stats.etapes}
{animated.ent}+
{cfg.stats.entreprises}
{animated.srv}
{cfg.stats.services}
{animated.pct}{cfg.emplois_suffixe}
')}} />
); } window.Screensaver = Screensaver;