// ProjectsLog — projets.html v2 : « Le registre des expéditions ». // Index éditorial du carnet : chaque projet de window.EXPEDITION est une // ligne du registre (titre display, colonnes mono, tampon biome), un aperçu // polaroid suit le curseur au survol, la fiche (ProjectModal, partagée avec // l'accueil via ProjectsExpedition.jsx) s'ouvre au clic. // Styles : projets.css (base partagée dans styles.css). // NB : ne jamais nommer une constante « EXPEDITION » ici — Babel compile les // const en `var` globales et cela écraserait window.EXPEDITION. const LOG_STEPS = window.EXPEDITION || []; // "+600 M" → 600 (dénivelé cumulé du registre) const LOG_ALT = LOG_STEPS.reduce( (sum, e) => sum + (parseInt(String(e.alt).replace(/\D/g, ""), 10) || 0), 0 ); // Biomes dans l'ordre d'apparition (onglets de classement) const LOG_BIOMES = LOG_STEPS.reduce( (list, e) => (list.includes(e.biome) ? list : [...list, e.biome]), [] ); const LOG_HOME = "Vincent%20Morin%20Portfolio.html#projets"; const LOG_REDUCE = window.matchMedia("(prefers-reduced-motion: reduce)").matches; // Projet mis en vitrine sur la couverture (le polaroid « dernier projet »). // Pour en mettre un autre en avant : changez l'id ci-dessous. const LOG_LATEST = LOG_STEPS.find(e => e.id === "plane-3d") || LOG_STEPS[0]; // Payload de la fiche modale — même forme que sur l'accueil const logToCard = (e) => ({ id: e.id, title: e.title, img: e.img, video: e.video, date: e.subtitle.split(" · ")[1] || "", stamp: e.n, role: e.subtitle, desc: e.desc, tags: e.tags, url: e.url || "#", }); /* ---------- Nav : retour au carnet ---------- */ function LogNav() { const [stuck, setStuck] = React.useState(false); const [dark, setDark] = React.useState(true); React.useEffect(() => { const onScroll = () => { const y = window.scrollY; setStuck(y > 40); // Crème tant qu'on est sur la photo de l'en-tête, forest ensuite const head = document.querySelector(".vpl-head"); setDark(y < (head ? head.offsetHeight - 130 : 400)); }; onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll); return () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); }; }, []); const cls = `vm-topnav vpl-nav ${dark ? "is-dark" : "is-light"} ` + `${stuck ? "is-stuck" : ""} ${stuck && dark ? "is-dark-stuck" : ""}`; return (
Retour au carnet REGISTRE DES EXPÉDITIONS · 2024 — 2026
); } /* ---------- Une ligne du registre ---------- */ function LogRow({ e, idx, onOpen, onHover }) { const open = () => onOpen(logToCard(e)); return (
  • ); } /* ---------- Page ---------- */ function ProjectsLogApp() { const [open, setOpen] = React.useState(null); const [biome, setBiome] = React.useState("TOUS"); const [preview, setPreview] = React.useState(null); const previewRef = React.useRef(null); const firstRender = React.useRef(true); const steps = biome === "TOUS" ? LOG_STEPS : LOG_STEPS.filter(e => e.biome === biome); // La couverture s'enfonce et s'estompe quand le registre passe par-dessus // (même effet que le hero de l'accueil, piloté ici par un simple scroll) React.useEffect(() => { if (!window.gsap || LOG_REDUCE) return; const inner = document.querySelector(".vpl-head-inner"); if (!inner) return; const yTo = gsap.quickTo(inner, "yPercent", { duration: 0.3, ease: "power1.out" }); const oTo = gsap.quickTo(inner, "opacity", { duration: 0.3, ease: "power1.out" }); const onScroll = () => { const p = Math.min(1, Math.max(0, window.scrollY / (window.innerHeight * 0.9))); yTo(-12 * p); oTo(1 - 0.75 * p); }; onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); // L'aperçu polaroid suit le curseur (quickTo = fluide, sans re-render) React.useEffect(() => { if (!window.gsap || !previewRef.current) return; const xTo = gsap.quickTo(previewRef.current, "x", { duration: 0.5, ease: "power3" }); const yTo = gsap.quickTo(previewRef.current, "y", { duration: 0.5, ease: "power3" }); const move = (ev) => { xTo(ev.clientX + 30); yTo(ev.clientY - 110); }; window.addEventListener("mousemove", move, { passive: true }); return () => window.removeEventListener("mousemove", move); }, []); // Apparition / disparition de l'aperçu React.useEffect(() => { if (!window.gsap || !previewRef.current) return; gsap.to(previewRef.current, { autoAlpha: preview ? 1 : 0, scale: preview ? 1 : 0.85, rotation: preview ? 0 : -5, duration: 0.35, ease: "power3.out", overwrite: true, }); }, [preview ? preview.id : null]); // Entrée de page + cascade des lignes (rejouée à chaque classement). // La révélation au scroll passe par IntersectionObserver (natif) : fiable // même sur un saut instantané (ancre, touche Fin), là où un batch // ScrollTrigger peut rater des lignes. React.useEffect(() => { if (!window.gsap || LOG_REDUCE) return; if (firstRender.current) { firstRender.current = false; gsap.from(".vpl-head-kicker, .vpl-head-title, .vpl-head-lead", { y: 44, autoAlpha: 0, duration: 0.9, stagger: 0.12, ease: "power4.out", }); // Le polaroid n'est pas animé : il est simplement là dès l'ouverture. gsap.from(".vpl-stat", { y: 26, autoAlpha: 0, duration: 0.6, stagger: 0.08, delay: 0.35, ease: "power3.out", }); const rows = gsap.utils.toArray(".vpl-row"); gsap.set(rows, { autoAlpha: 0, y: 40 }); const io = new IntersectionObserver((entries) => { entries.filter(en => en.isIntersecting).forEach((en, i) => { gsap.to(en.target, { autoAlpha: 1, y: 0, duration: 0.7, delay: i * 0.09, ease: "power3.out", overwrite: true, }); io.unobserve(en.target); }); }, { rootMargin: "0px 0px -5% 0px" }); rows.forEach(r => io.observe(r)); } else { gsap.fromTo(".vpl-row", { autoAlpha: 0, y: 26 }, { autoAlpha: 1, y: 0, duration: 0.5, stagger: 0.06, ease: "power3.out", overwrite: true, clearProps: "transform" }); } }, [biome]); return ( <>
    REGISTRE OUVERT 2024 — 2026
    {/* Druk Trial (display) n'a pas les glyphes accentués — comme sur l'accueil (« Decouvre »), les textes en display évitent les accents ; les textes en mono/body les gardent. */}

    Le registre
    des projets.

    {/* Polaroid scotché : le dernier projet, en vitrine sur la couverture */} {LOG_LATEST && ( )}

    Tout ce qui a été gravi depuis 2024 est consigné ici, ligne par ligne : l'entreprise, l'agence, les cours et les explorations en solo. Survolez une ligne pour l'aperçu, cliquez pour ouvrir la fiche de l'étape.

    ÉTAPES
    {LOG_STEPS.length}
    PÉRIODE
    2024 - 2026
    {/* .vpl-body glisse par-dessus la couverture fixe (cf. accueil) */}
    FIN DU REGISTRE — MERCI ! Redescendre au camp de base
    {/* /.vpl-body */} {/* aperçu polaroid flottant — masqué sur écrans tactiles */} {open && setOpen(null)} />} ); } ReactDOM.createRoot(document.getElementById("app")).render();