// NAVIGATION ET ANIMATIONS INTERACTIVES
let sunHasDisappeared = false;
/**
* Déplace la tête et les yeux en fonction de la position de la souris
* @param {MouseEvent} e - L'événement de mouvement de souris
* @constant head {HTMLElement} - L'élément de la tête
* @constant eyeLeft {HTMLElement} - L'élément de l'œil gauche
* @constant eyeRight {HTMLElement} - L'élément de l'œil droit
* @constant centerX {number} - La position X du centre de la fenêtre7
* @constant centerY {number} - La position Y du centre de la fenêtre
* @constant dx {number} - La différence entre la position de la souris et le centre de la fenêtre sur l'axe X
* @constant dy {number} - La différence entre la position de la souris et le centre de la fenêtre sur l'axe Y
* @description Déplace la tête et les yeux en fonction de la position de la souris
* @returns {void}
*/
document.addEventListener('mousemove', (e) => {
const head = document.getElementById('head');
const eyeLeft = document.getElementById('eye-left');
const eyeRight = document.getElementById('eye-right');
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
const dx = (e.clientX - centerX) / 50;
const dy = (e.clientY - centerY) / 50;
head.style.transform = `translate(${dx}px, ${dy}px)`;
eyeLeft.style.transform = `translate(${dx}px, ${dy}px)`;
eyeRight.style.transform = `translate(${dx}px, ${dy}px)`;
});
/**
* Anime le clignement des yeux toutes les 4 secondes
* @constant leftEye {HTMLElement} - L'élément de l'œil gauche
* @constant rightEye {HTMLElement} - L'élément de l'œil droit
* @description Anime le clignement des yeux en ajoutant et en supprimant la classe 'blink'
* @returns {void}
*/
setInterval(() => {
const leftEye = document.getElementById('eye-left');
const rightEye = document.getElementById('eye-right');
leftEye.classList.add('blink');
rightEye.classList.add('blink');
setTimeout(() => {
leftEye.classList.remove('blink');
rightEye.classList.remove('blink');
}, 200);
}, 4000);
/**
* Gère le clic sur le soleil : fait disparaître le soleil, change la couleur du ciel,
* masque la goutte, affiche un sourire et lance la neige et les messages climatiques
* @constant sun {HTMLElement} - L'élément du soleil
* @constant bubble {HTMLElement} - L'élément de la bulle de parole
* @description Gère le clic sur le soleil
* @returns {void}
*/
document.getElementById('sun').addEventListener('click', () => {
const sun = document.getElementById('sun');
sun.style.opacity = '0';
setTimeout(() => sun.style.display = 'none', 800);
document.body.style.setProperty('--sky-color', '#a2ddee');
document.querySelector('.droplet-css').style.display = 'none';
document.getElementById('mouth').classList.add('smile');
const bubble = document.getElementById('speech-bubble');
bubble.innerHTML = `
<p>Ahhh… C'est bien mieux maintenant ❄️</p>
<p>Est-ce que tu sais que...</p>
<div id="messages" style="margin-top: 12px;"></div>
`;
startSnowfall();
startClimateMessages();
sunHasDisappeared = true;
});
/**
* Lance l'effet de chute de flocons de neige sur la page
* @constant containerClass {string} - La classe du conteneur de flocons de neige
* @constant snowflakeChars {string[]} - Les caractères de flocons de neige
* @constant colors {string[]} - Les couleurs des flocons de neige
* @constant MAX_FLAKES {number} - Le nombre maximum de flocons de neige
* @constant currentFlakes {number} - Le nombre actuel de flocons de neige
* @description Crée un conteneur pour les flocons de neige et les anime
* @returns {void}
*/
function startSnowfall() {
const containerClass = "snowfall";
let container = document.querySelector(`.${containerClass}`);
if (!container) {
container = document.createElement("div");
container.className = containerClass;
document.body.appendChild(container);
}
const snowflakeChars = ["❄", "❅", "❆", "✻", "✼"];
const colors = ["white", "#cceeff", "#99ddff"];
const MAX_FLAKES = 100;
let currentFlakes = 0;
/**
* Retourne un nombre aléatoire entre min et max
* @param {number} min
* @param {number} max
* @description Retourne un nombre aléatoire entre min et max
* @returns {number}
*/
function randomBetween(min, max) {
return Math.random() * (max - min) + min;
}
/**
* Crée un flocon de neige et l'ajoute au conteneur
* @constant flake {HTMLElement} - L'élément du flocon de neige
* @constant size {number} - La taille du flocon de neige
* @description Crée un flocon de neige et l'ajoute au conteneur
* @returns {void}
*/
function createFlake() {
if (currentFlakes >= MAX_FLAKES) return;
const flake = document.createElement("span");
flake.className = "snowflake";
flake.textContent = snowflakeChars[Math.floor(Math.random() * snowflakeChars.length)];
const size = randomBetween(12, 28);
flake.style.left = `${randomBetween(0, 100)}vw`;
flake.style.fontSize = `${size}px`;
flake.style.opacity = randomBetween(0.5, 1).toFixed(2);
flake.style.color = colors[Math.floor(Math.random() * colors.length)];
flake.style.animation = `
fall ${randomBetween(6, 12)}s linear infinite,
sway ${randomBetween(2, 5)}s ease-in-out infinite alternate
`;
container.appendChild(flake);
currentFlakes++;
setTimeout(() => {
flake.remove();
currentFlakes--;
}, 12000);
}
setInterval(createFlake, 200);
}
/**
* Affiche des messages climatiques dans la bulle de parole, cycliquement
* @constant messages {string[]} - Les messages climatiques
* @const container {HTMLElement} - L'élément de la bulle de parole
* @description Affiche des messages climatiques dans la bulle de parole
* @returns {void}
*/
function startClimateMessages() {
const messages = [
"🌡️ L'Arctique se réchauffe 4x plus vite.",
"🧊 75% de la glace de mer a disparu.",
"🐻❄️ J'ai besoin de glace pour survivre.",
"🚿 Une douche rapide = -60L d’eau !",
"🌍 Chaque geste compte pour le climat."
];
let current = 0;
const container = document.getElementById('messages');
setTimeout(() => {
container.textContent = messages[current];
setInterval(() => {
current = (current + 1) % messages.length;
container.textContent = messages[current];
}, 8000);
}, 5000);
}
/**
* Affiche un cœur animé lorsque la tête est cliquée (si le soleil a disparu)
* @constant wrap {HTMLElement} - L'élément de l'enveloppe
* @constant head {HTMLElement} - L'élément de la tête
* @constant heart {HTMLElement} - L'élément du cœur
* @description Affiche un cœur animé lorsque la tête est cliquée
* @returns {void}
*/
document.getElementById('head').addEventListener('click', () => {
const wrap = document.querySelector('.wrap');
const head = document.getElementById('head');
if (sunHasDisappeared) {
const heart = document.createElement('div');
heart.className = 'heart-pop';
heart.textContent = '❤️';
wrap.appendChild(heart);
head.classList.remove('happy-animate');
void head.offsetWidth;
head.classList.add('happy-animate');
setTimeout(() => heart.remove(), 1000);
}
});
// === CAROUSEL 3D NAVIGATION ===
const carousel = document.querySelector('.carousel');
const items = document.querySelectorAll('.carousel-item');
let currentItem = 0;
/**
* Met à jour la position du carrousel en fonction de l'élément courant
* @constant offset {number} - L'offset de translation en pourcentage
* @description Met à jour la position du carrousel
* @returns {void}
*/
function updateCarousel() {
const offset = -currentItem * 100;
carousel.style.transform = `translateX(${offset}%)`;
}
/**
* Gère le clic sur le bouton "suivant" du carrousel
* @description Gère le clic sur le bouton "suivant"
* @returns {void}
*/
document.querySelector('.carousel-btn.next').addEventListener('click', () => {
currentItem = (currentItem + 1) % items.length;
updateCarousel();
});
/**
* Gère le clic sur le bouton "précédent" du carrousel
* @description Gère le clic sur le bouton "précédent"
* @returns {void}
*/
document.querySelector('.carousel-btn.prev').addEventListener('click', () => {
currentItem = (currentItem - 1 + items.length) % items.length;
updateCarousel();
});
// Redirige vers des liens externes lors du clic sur les cartes de catégories d'impacts
/**
* Tableau des liens externes pour chaque catégorie d'impact
* @constant impactLinks {string[]} - Liens vers des articles externes pour chaque catégorie d'impact
* @description Liens vers des articles externes pour chaque catégorie d'impact
* @type {string[]}
*/
const impactLinks = [
// Fonte des glaces
"https://www.lemonde.fr/planete/article/2025/02/19/les-glaciers-declinent-partout-autour-du-globe-perdant-l-equivalent-de-trois-piscines-olympiques-par-seconde_6554635_3244.html",
// Canicules & Incendies
"https://www.futura-sciences.com/planete/actualites/climatologie-2024-ete-annee-tous-extremes-118314/",
// Catastrophes naturelles
"https://www.futura-sciences.com/planete/actualites/rechauffement-climatique-570-000-morts-voici-pires-catastrophes-meteo-depuis-20-ans-europe-surrepresentee-117268/",
// Santé humaine
"https://www.who.int/fr/news-room/fact-sheets/detail/climate-change-and-health",
// Agriculture
"https://www.actu-environnement.com/ae/news/adaptation-climat-agriculture-filieres-exploitations-cultures-eau-45679.php4",
// Biodiversité
"https://www.wwf.fr/vous-informer/actualites/rapport-planete-vivante-2024-les-populations-de-vertebres-sauvages-ont-decline-de-73-depuis-1970",
];
/**
* Ajoute un événement de clic sur chaque carte d'impact pour ouvrir le lien correspondant
* @description Ajoute un événement de clic sur chaque carte d'impact
* @returns {void}
*/
document.querySelectorAll('.rubriques .card').forEach((card, idx) => {
card.style.cursor = "pointer";
card.addEventListener('click', () => {
window.open(impactLinks[idx], '_blank');
});
});
/**
* Tableau des liens externes pour les articles d'actualité
* @constant newsLinks {string[]} - Liens vers des articles d'actualité externes
* @description Liens vers des articles d'actualité externes
* @type {string[]}
*/
const newsLinks = [
// Jakarta menacée par la montée des eaux
"https://www.lemonde.fr/planete/article/2025/03/06/a-djakarta-les-inondations-rappellent-la-menace-de-submersion-et-les-risques-de-la-sururbanisation_6576817_3244.html",
// Été 2024 : le plus chaud jamais enregistré
"https://www.lemonde.fr/planete/article/2024/09/06/l-ete-2024-est-le-plus-chaud-jamais-enregistre-dans-le-monde_6305244_3244.html"
];
/**
* Ajoute un événement de clic sur chaque article d'actualité pour ouvrir le lien correspondant
* @description Ajoute un événement de clic sur chaque article d'actualité
* @returns {void}
*/
document.querySelectorAll('.news.block article').forEach((article, idx) => {
article.style.cursor = "pointer";
article.addEventListener('click', () => {
window.open(newsLinks[idx], '_blank');
});
});
/**
* Tableau des liens externes pour les boutons du slider
* @constant sliderLinks {string[]} - Liens vers des articles externes pour chaque catégorie d'impact
* @description Liens vers des articles externes pour chaque catégorie d'impact
* @type {string[]}
*/
const sliderLinks = [
// Sécheresse extrême
"https://rhone-mediterranee.eaufrance.fr/aout-2024-la-secheresse-se-declenche-sur-le-bassin",
// Tempêtes violentes
"https://meteofrance.com/actualites-et-dossiers/comprendre-la-meteo/les-tempetes-remarquables-en-france",
// Écosystèmes détruits
"https://www.greenpeace.fr/deforestation/"
];
/**
* Ajoute un événement de clic sur chaque bouton du slider pour ouvrir le lien correspondant
* @description Ajoute un événement de clic sur chaque bouton du slider
* @returns {void}
*/
document.querySelectorAll('.carousel .carousel-item button').forEach((btn, idx) => {
btn.addEventListener('click', () => {
window.open(sliderLinks[idx], '_blank');
});
});
Source