// 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) */}