Tu veux apprendre à faire un truc sympa ?

[su_image_carousel source= »media: 476,475,474″ limit= »16″]

Pour apprendre à coder, on commence souvent par des exemples. Le plus connu c’est Hello World ! On apprend à afficher ça à l’écran.

Le code c’est bien mais la mise en forme est primordiale.

Pour ça je t’ai préparé une petite appli simple avec HTML, CSS et une lichette de javascript.

Le principe, une page qui affiche les différentes phases de la lune et qui se met à jour toutes les heures si jamais il te prend l’idée de rester bloqué sur ton écran pendant quelques semaines.

Tu trouveras donc cher ami lecteur le code complet que je t’ai commenté pour que tu comprennes ce que j’ai fait.

Si tu veux voir le résultat → c’est ICI

[su_quote]Ce que tu vas pouvoir utiliser pour pousser plus loin : Les balises HTML et CSS : Mozilla t’explique tout ! Pour le javascript, c’est pareil ! Les cours Mozilla t’aideront[/su_quote]

 

<!DOCTYPE html>
<!–
Cadran lunaire — version commentée
————————————
J’ai voulu un mono-fichier HTML : pas de build, pas de dépendances,
je le pose sur n’importe quel hébergeur statique et ça marche.
Tout est inline : HTML, CSS, JS dans le même document.
–>
<html lang= »fr »>
<head>
<meta charset= »UTF-8″>
<!– Sans cette ligne, le responsive ne fonctionne pas sur mobile : le navigateur
simulerait un écran de 980px et tout serait minuscule. –>
<meta name= »viewport » content= »width=device-width, initial-scale=1.0″>
<title>Cadran lunaire</title>

<!– preconnect : j’ouvre le canal vers Google Fonts pendant que le navigateur
parse le HTML. Ça gagne ~200 ms sur le chargement des polices. –>
<link rel= »preconnect » href= »https://fonts.googleapis.com »>
<link rel= »preconnect » href= »https://fonts.gstatic.com » crossorigin>

<!– Mes choix typographiques : Cormorant pour les titres (serif élégant,
évite le piège des polices « AI slop » type Inter ou Roboto) et EB Garamond
pour le corps. Les deux sont de la famille Garamond, cohérent visuellement. –>
<link href= »https://fonts.googleapis.com/css2?family=Cormorant:ital,wght@0,300;0,400;0,500;1,300&family=EB+Garamond:ital,wght@0,400;0,500;1,400&display=swap » rel= »stylesheet »>

<style>
/* ──────────────────────────────────────────────────────
VARIABLES CSS
Je les centralise ici pour pouvoir changer toute la palette
en modifiant 5 lignes. Discipline de base.
────────────────────────────────────────────────────── */
:root {
–bg-deep: #0a0e1a; /* fond presque noir, légèrement bleuté */
–bg-surface: #141828; /* surfaces (cartes) un cran plus claires */
–bg-card: #1a1f33; /* hover des cartes */
–ink: #e8dcc4; /* texte principal : crème vieux papier */
–ink-muted: #8a8068; /* texte secondaire */
–ink-faint: #4a4538; /* texte très faible (footer) */
–gold: #c9a961; /* accent unique : or antique */
–gold-soft: #a88d4a; /* variante de l’or pour les bordures */
–moon-lit: #f4ecd8; /* face éclairée de la lune : crème chaud */
–moon-dark: #1c2138; /* face sombre : pas noire, pour rester lisible */
–rule: rgba(201, 169, 97, 0.18); /* lignes de séparation à l’or */
}

/* Reset minimal. Je n’utilise pas Normalize ou Eric Meyer parce que
pour un fichier de cette taille c’est overkill. */
* { box-sizing: border-box; margin: 0; padding: 0; }

html, body {
background: var(–bg-deep);
color: var(–ink);
font-family: ‘EB Garamond’, Georgia, serif;
/* Si Google Fonts ne charge pas, je tombe sur Georgia : c’est pas mal du tout. */
font-size: 17px;
line-height: 1.55;
min-height: 100vh;
overflow-x: hidden;
}

/* ──────────────────────────────────────────────────────
ÉTOILES EN ARRIÈRE-PLAN
Je dessine 12 étoiles avec des radial-gradient empilés.
Pas de JS, pas de SVG : c’est du CSS pur, rendu instantané.
────────────────────────────────────────────────────── */
body::before {
content:  »;
position: fixed;
inset: 0;
background-image:
radial-gradient(1px 1px at 12% 18%, rgba(232,220,196,.6), transparent),
radial-gradient(1px 1px at 78% 9%, rgba(232,220,196,.5), transparent),
radial-gradient(1px 1px at 33% 71%, rgba(232,220,196,.4), transparent),
radial-gradient(1px 1px at 92% 47%, rgba(232,220,196,.7), transparent),
radial-gradient(1px 1px at 5% 89%, rgba(232,220,196,.5), transparent),
radial-gradient(1px 1px at 64% 32%, rgba(232,220,196,.45), transparent),
radial-gradient(1px 1px at 47% 95%, rgba(232,220,196,.4), transparent),
radial-gradient(1px 1px at 20% 50%, rgba(232,220,196,.35), transparent),
radial-gradient(1.5px 1.5px at 85% 75%, rgba(232,220,196,.55), transparent),
radial-gradient(1px 1px at 55% 12%, rgba(232,220,196,.5), transparent),
radial-gradient(1px 1px at 70% 60%, rgba(232,220,196,.3), transparent),
radial-gradient(1px 1px at 38% 38%, rgba(232,220,196,.4), transparent);
pointer-events: none; /* important : sinon les étoiles bloqueraient les clics */
z-index: 0;
}

/* ──────────────────────────────────────────────────────
VOILE ATMOSPHÉRIQUE
Deux gradients : un halo or léger en haut, un assombrissement en bas.
C’est ce qui transforme un fond plat en « scène » avec profondeur.
────────────────────────────────────────────────────── */
body::after {
content:  »;
position: fixed;
inset: 0;
background:
radial-gradient(ellipse 80% 60% at 50% 30%, rgba(201, 169, 97, 0.05), transparent),
radial-gradient(ellipse 60% 80% at 50% 100%, rgba(20, 24, 40, 0.6), transparent);
pointer-events: none;
z-index: 0;
}

/* Le contenu doit passer au-dessus des deux pseudo-éléments du body */
main {
position: relative;
z-index: 1;
max-width: 920px; /* largeur lisible pour du texte de lecture */
margin: 0 auto;
padding: 60px 32px 80px;
}

/* ──────────────────────────────────────────────────────
EN-TÊTE
Un ✦ unicode posé sur une ligne horizontale,
avec le fond de page derrière, donne l’illusion qu’il « coupe » la ligne.
────────────────────────────────────────────────────── */
.header {
text-align: center;
margin-bottom: 60px;
padding-bottom: 40px;
border-bottom: 1px solid var(–rule);
position: relative;
}

.header::after {
content: ‘✦’;
position: absolute;
bottom: -10px; /* posé pile sur la ligne */
left: 50%;
transform: translateX(-50%);
background: var(–bg-deep); /* le fond cache la ligne derrière le caractère */
color: var(–gold);
padding: 0 14px;
font-size: 14px;
}

/* Le « eyebrow » est une typo italique en petites capitales : très almanach ancien. */
.eyebrow {
font-family: ‘Cormorant’, serif;
font-style: italic;
font-weight: 300;
color: var(–gold-soft);
letter-spacing: 0.18em;
text-transform: uppercase;
font-size: 11px;
margin-bottom: 12px;
}

/* Titre fluide grâce à clamp() : pas besoin de media queries pour ça.
Min 48px (mobile), max 88px (grand écran), entre les deux 9% de la largeur. */
h1 {
font-family: ‘Cormorant’, serif;
font-weight: 300; /* light : volontairement, ça donne un côté éthéré */
font-size: clamp(48px, 9vw, 88px);
letter-spacing: 0.02em;
color: var(–ink);
line-height: 1;
}

.date-line {
margin-top: 18px;
font-style: italic;
color: var(–ink-muted);
font-size: 16px;
}

/* ──────────────────────────────────────────────────────
SECTION HÉRO : LA LUNE EN GRAND
────────────────────────────────────────────────────── */
.hero {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 80px;
}

.moon-stage {
position: relative;
width: 280px;
height: 280px;
margin-bottom: 36px;
}

/* Halo doux autour de la lune. ::before étend une zone translucide
au-delà de la SVG. */
.moon-stage::before {
content:  »;
position: absolute;
inset: -40px;
background: radial-gradient(circle, rgba(244, 236, 216, 0.08), transparent 70%);
border-radius: 50%;
pointer-events: none;
}

/* drop-shadow ajoute une lueur autour de la silhouette de la lune
(et pas autour de son rectangle englobant comme box-shadow). */
.moon-svg {
width: 100%;
height: 100%;
filter: drop-shadow(0 0 20px rgba(244, 236, 216, 0.15));
}

.moon-disc {
fill: var(–moon-dark);
stroke: rgba(201, 169, 97, 0.25); /* fin liseré or pour souligner le disque */
stroke-width: 0.5;
}

.moon-lit { fill: var(–moon-lit); }

/* Bloc de données sous la lune */
.data-panel { text-align: center; max-width: 540px; }

.phase-name {
font-family: ‘Cormorant’, serif;
font-weight: 400;
font-size: clamp(28px, 5vw, 38px);
color: var(–ink);
margin-bottom: 8px;
letter-spacing: 0.01em;
}

.phase-meta {
color: var(–ink-muted);
font-style: italic;
font-size: 16px;
margin-bottom: 24px;
}

/* ──────────────────────────────────────────────────────
GRILLE DE STATS
Astuce : gap: 1px + background sur le parent crée des séparateurs
entre les cellules sans avoir à gérer de border-right (qui pose
toujours des problèmes de double-bordure aux bords).
────────────────────────────────────────────────────── */
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1px;
background: var(–rule); /* visible dans les « gaps » */
border-top: 1px solid var(–rule);
border-bottom: 1px solid var(–rule);
}

.stat {
background: var(–bg-deep); /* recouvre l’or sauf dans les gaps */
padding: 18px 12px;
}

.stat-value {
font-family: ‘Cormorant’, serif;
font-size: 30px;
color: var(–gold);
font-weight: 400;
line-height: 1;
margin-bottom: 6px;
}

.stat-label {
font-size: 11px;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(–ink-muted);
}

/* ──────────────────────────────────────────────────────
SECTIONS GÉNÉRIQUES (folklore, phases, éclipses)
────────────────────────────────────────────────────── */
section.block { margin-bottom: 64px; }

/* Numérotation romaine + titre, séparés par un espace : référence aux
ouvrages anciens où chaque chapitre porte son numéro romain. */
.section-head {
display: flex;
align-items: baseline;
gap: 20px;
margin-bottom: 32px;
padding-bottom: 16px;
border-bottom: 1px solid var(–rule);
}

.section-num {
font-family: ‘Cormorant’, serif;
font-style: italic;
color: var(–gold);
font-size: 14px;
letter-spacing: 0.1em;
}

.section-title {
font-family: ‘Cormorant’, serif;
font-weight: 400;
font-size: 28px;
letter-spacing: 0.02em;
}

/* ──────────────────────────────────────────────────────
CARTE FOLKLORE — coins ornementaux
Je dessine deux « L » inversés avec ::before et ::after,
en cachant deux côtés de la bordure pour ne garder qu’un coin.
Référence visuelle aux gravures anciennes.
────────────────────────────────────────────────────── */
.folklore-card {
background: var(–bg-surface);
border: 1px solid var(–rule);
padding: 36px;
text-align: center;
position: relative;
}

.folklore-card::before,
.folklore-card::after {
content:  »;
position: absolute;
width: 24px;
height: 24px;
border: 1px solid var(–gold);
}
.folklore-card::before {
top: 8px; left: 8px;
border-right: none; /* on cache 2 côtés sur 4 */
border-bottom: none; /* il reste un L en haut-gauche */
}
.folklore-card::after {
bottom: 8px; right: 8px;
border-left: none;
border-top: none; /* L symétrique en bas-droite */
}

.folk-name {
font-family: ‘Cormorant’, serif;
font-style: italic;
font-size: 36px;
color: var(–gold);
margin-bottom: 12px;
}

.folk-desc {
color: var(–ink);
max-width: 520px; /* je limite la largeur pour la lisibilité du texte */
margin: 0 auto;
}

.folk-date {
margin-top: 18px;
font-size: 13px;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(–ink-muted);
}

/* ──────────────────────────────────────────────────────
GRILLE DES PHASES À VENIR
auto-fit + minmax fait que la grille se réorganise toute seule :
4 colonnes en desktop, 2 en tablette, 1 en mobile, sans media query.
────────────────────────────────────────────────────── */
.phases-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1px;
background: var(–rule);
border: 1px solid var(–rule);
}

.phase-card {
background: var(–bg-surface);
padding: 28px 20px;
text-align: center;
transition: background 0.3s; /* hover doux */
}

.phase-card:hover { background: var(–bg-card); }

.phase-icon { width: 56px; height: 56px; margin: 0 auto 16px; }

.phase-card-name {
font-family: ‘Cormorant’, serif;
font-size: 19px;
color: var(–ink);
margin-bottom: 10px;
}

.phase-card-date {
color: var(–gold);
font-size: 15px;
font-style: italic;
}

.phase-card-rel {
margin-top: 6px;
font-size: 12px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(–ink-muted);
}

/* ──────────────────────────────────────────────────────
LISTE DES ÉCLIPSES
Grille à 3 colonnes : date | description | étiquette.
────────────────────────────────────────────────────── */
.eclipse-list { display: flex; flex-direction: column; }

.eclipse-item {
display: grid;
grid-template-columns: 100px 1fr auto;
gap: 24px;
align-items: center;
padding: 22px 0;
border-bottom: 1px solid var(–rule);
}

.eclipse-item:last-child { border-bottom: none; }

.eclipse-date {
font-family: ‘Cormorant’, serif;
color: var(–gold);
font-size: 22px;
text-align: right;
line-height: 1.1;
}

.eclipse-date small {
display: block;
font-size: 12px;
color: var(–ink-muted);
letter-spacing: 0.1em;
margin-top: 4px;
font-family: ‘EB Garamond’, serif;
}

.eclipse-name { font-family: ‘Cormorant’, serif; font-size: 19px; }

.eclipse-desc {
font-size: 14px;
color: var(–ink-muted);
font-style: italic;
margin-top: 4px;
}

/* L’étiquette (Soleil/Lune) est en or pour les éclipses solaires,
en crème pour les lunaires. Distinction visuelle immédiate. */
.eclipse-tag {
font-size: 11px;
letter-spacing: 0.15em;
text-transform: uppercase;
padding: 4px 10px;
border: 1px solid var(–gold-soft);
color: var(–gold);
white-space: nowrap;
}

.eclipse-tag.lunar { border-color: rgba(244, 236, 216, 0.3); color: var(–ink); }

/* Pied de page discret */
footer {
margin-top: 80px;
padding-top: 30px;
border-top: 1px solid var(–rule);
text-align: center;
font-size: 13px;
color: var(–ink-faint);
font-style: italic;
}

/* ──────────────────────────────────────────────────────
RESPONSIVE — une seule media query
Tout ce qui pouvait être géré par clamp() ou auto-fit l’est déjà.
Cette breakpoint gère uniquement ce qui doit vraiment changer
de structure en mobile.
────────────────────────────────────────────────────── */
@media (max-width: 600px) {
main { padding: 40px 20px 60px; }
.moon-stage { width: 220px; height: 220px; }
.stats { grid-template-columns: 1fr; } /* stats empilées */
.stat { padding: 14px; }
.eclipse-item { grid-template-columns: 80px 1fr; }
.eclipse-tag { grid-column: 2; justify-self: start; }
.folklore-card { padding: 28px 20px; }
.folk-name { font-size: 28px; }
}
</style>
</head>
<body>
<main>

<!– ────────────────────────────────────────────────────
En-tête : eyebrow + titre + date du jour
──────────────────────────────────────────────────── –>
<header class= »header »>
<div class= »eyebrow »>Almanach des phases &amp; éclipses</div>
<h1>Cadran lunaire</h1>
<div class= »date-line » id= »today-date »></div>
</header>

<!– ────────────────────────────────────────────────────
Section héro : la lune en grand + données principales
Le SVG a un viewBox -110/-110/220/220 : centré sur (0,0),
la lune (rayon 100) tient avec une marge de 10 unités tout autour.
──────────────────────────────────────────────────── –>
<section class= »hero »>
<div class= »moon-stage »>
<svg class= »moon-svg » id= »moon » viewBox= »-110 -110 220 220″ xmlns= »http://www.w3.org/2000/svg »>
<!– Disque sombre (face cachée) toujours dessiné en premier –>
<circle class= »moon-disc » cx= »0″ cy= »0″ r= »100″></circle>
<!– Path de la portion éclairée, généré dynamiquement par JS –>
<path class= »moon-lit » id= »moon-lit-path » d= » »></path>
</svg>
</div>
<div class= »data-panel »>
<div class= »phase-name » id= »phase-name »>—</div>
<div class= »phase-meta » id= »phase-meta »>—</div>
<div class= »stats »>
<div class= »stat »>
<div class= »stat-value » id= »illum »>—</div>
<div class= »stat-label »>Illumination</div>
</div>
<div class= »stat »>
<div class= »stat-value » id= »age »>—</div>
<div class= »stat-label »>Âge (jours)</div>
</div>
<div class= »stat »>
<div class= »stat-value » id= »cycle »>—</div>
<div class= »stat-label »>Cycle</div>
</div>
</div>
</div>
</section>

<!– Section I — Folklore –>
<section class= »block »>
<div class= »section-head »>
<span class= »section-num »>I.</span>
<span class= »section-title »>Pleine lune du mois</span>
</div>
<div class= »folklore-card »>
<div class= »folk-name » id= »folk-name »>—</div>
<div class= »folk-desc » id= »folk-desc »>—</div>
<div class= »folk-date » id= »folk-date »>—</div>
</div>
</section>

<!– Section II — Prochaines phases (rempli par JS) –>
<section class= »block »>
<div class= »section-head »>
<span class= »section-num »>II.</span>
<span class= »section-title »>Prochaines phases</span>
</div>
<div class= »phases-grid » id= »phases-grid »></div>
</section>

<!– Section III — Éclipses (rempli par JS) –>
<section class= »block »>
<div class= »section-head »>
<span class= »section-num »>III.</span>
<span class= »section-title »>Éclipses à venir</span>
</div>
<div class= »eclipse-list » id= »eclipse-list »></div>
</section>

<footer>
Calculs établis selon les algorithmes de Jean Meeus, simplifiés.<br>
Heures locales (Europe/Paris). Mise à jour à chaque chargement.
</footer>

</main>

<script>
‘use strict’;
/*
═══════════════════════════════════════════════════════════════════
PARTIE 1 — CALCULS ASTRONOMIQUES
═══════════════════════════════════════════════════════════════════

Tout repose sur deux constantes : la durée du mois synodique et
une nouvelle lune de référence. À partir de là, je peux situer
n’importe quelle date dans le cycle lunaire.

Modèle simplifié : je considère que les nouvelles lunes se succèdent
à intervalles strictement égaux. Dans la réalité elles oscillent de
± quelques heures à cause de l’orbite elliptique de la Lune. Pour
un cadran d’usage courant, l’approximation est largement suffisante.
*/

// Durée moyenne entre deux nouvelles lunes : 29 jours, 12 h, 44 min, 2.8 s
const SYNODIC = 29.530588853;

// Nouvelle lune du 6 janvier 2000 à 18:14 UT, exprimée en jour julien.
// J’ai choisi cette époque parce que c’est la référence standard
// utilisée par Jean Meeus dans « Astronomical Algorithms ».
const REF_NEW_MOON_JD = 2451550.09766;

/*
Le jour julien (JD) compte les jours depuis le 1er janvier −4712 à midi UT.
Avantage : c’est un nombre continu, sans année bissextile à gérer,
sans changement de calendrier. Toute différence de dates devient
une simple soustraction.
*/

function dateToJD(d) {
// d.getTime() donne les millisecondes depuis le 1er janvier 1970.
// Je convertis en jours, puis j’ajoute le JD du 1er janvier 1970 (= 2440587.5)
return d.getTime() / 86400000 + 2440587.5;
}

function jdToDate(jd) {
// L’inverse : je convertis un JD en objet Date JavaScript
return new Date((jd – 2440587.5) * 86400000);
}

// Phase : nombre entre 0 et 1
// 0 = nouvelle lune
// 0.25 = premier quartier
// 0.5 = pleine lune
// 0.75 = dernier quartier
function moonPhase(jd) {
const cycles = (jd – REF_NEW_MOON_JD) / SYNODIC;
// Je ne garde que la partie fractionnaire : peu importe combien
// de cycles complets se sont écoulés depuis la référence.
return cycles – Math.floor(cycles);
}

// Âge : combien de jours se sont écoulés depuis la dernière nouvelle lune
function moonAge(jd) {
return moonPhase(jd) * SYNODIC;
}

/*
Pourcentage de la face visible qui est éclairée.
Formule classique : on suppose que l’angle de phase varie linéairement.
Au moment de la nouvelle lune, cos = 1 → illum = 0 (rien d’éclairé).
Au moment de la pleine lune, cos = -1 → illum = 1 (tout éclairé).
*/
function moonIllumination(phase) {
return (1 – Math.cos(2 * Math.PI * phase)) / 2;
}

/*
Trouver la prochaine occurrence d’une phase cible (0, 0.25, 0.5, 0.75).
Logique : combien de cycle reste-t-il à parcourir avant d’atteindre
la cible ? On multiplie par la durée d’un cycle, on ajoute au JD courant.
*/
function nextPhaseDate(fromJD, target) {
const cur = moonPhase(fromJD);
let diff = target – cur;
if (diff <= 0) diff += 1; // si la cible est « avant » dans le cycle, je passe au suivant
return jdToDate(fromJD + diff * SYNODIC);
}

/*
Étiquette française de la phase, avec une petite description.
J’utilise une tolérance epsilon pour distinguer « exactement quartier »
de « presque quartier ». 0.02 = environ 14 heures, ce qui me semble
une fenêtre raisonnable pour parler de « premier quartier » tel quel.
*/
function phaseLabel(p) {
const eps = 0.02;
if (p < eps || p > 1 – eps) return { name: ‘Nouvelle lune’, meta: ‘Aucun éclat ne touche la face visible’ };
if (Math.abs(p – 0.25) < eps) return { name: ‘Premier quartier’, meta: ‘La moitié droite illuminée’ };
if (Math.abs(p – 0.5) < eps) return { name: ‘Pleine lune’, meta: ‘Le disque entier rayonne’ };
if (Math.abs(p – 0.75) < eps) return { name: ‘Dernier quartier’, meta: ‘La moitié gauche illuminée’ };
if (p < 0.25) return { name: ‘Premier croissant’, meta: ‘Croissance de la lumière’ };
if (p < 0.5) return { name: ‘Lune gibbeuse croissante’, meta: ‘Lumière en expansion’ };
if (p < 0.75) return { name: ‘Lune gibbeuse décroissante’, meta: ‘Lumière en retrait’ };
return { name: ‘Dernier croissant’, meta: ‘La lumière s’efface’ };
}

/*
═══════════════════════════════════════════════════════════════════
PARTIE 2 — DESSIN DE LA LUNE EN SVG
═══════════════════════════════════════════════════════════════════

C’est le morceau le plus subtil. Je dessine la portion éclairée de la
lune comme un polygone fermé délimité par deux arcs :

1. Une moitié du cercle (le « limbe », bord extérieur de la lune)
2. Un demi-ellipse (le « terminateur », la frontière jour/nuit)

L’ellipse a toujours la même hauteur que le disque (ry = R), mais
sa largeur (rx) varie selon la phase :

• À la nouvelle lune (p=0) : rx = R → ellipse confondue avec le limbe → 0% éclairé
• Au premier quartier (p=0.25) : rx = 0 → terminateur vertical → 50% éclairé (à droite)
• À la pleine lune (p=0.5) : rx = R → ellipse de l’autre côté → 100% éclairé
• Au dernier quartier (p=0.75) : rx = 0 → terminateur vertical → 50% éclairé (à gauche)

La largeur signée k = cos(2π·phase) me donne tout :
• |k| → largeur de l’ellipse
• signe de k → bombement de l’ellipse (vers le côté éclairé ou non)

Les flags « sweep » en SVG indiquent dans quel sens parcourir l’arc.
C’est ce qui détermine si on délimite la zone éclairée ou la zone sombre.
*/
function moonPath(phase, R) {
const k = Math.cos(2 * Math.PI * phase);
const rx = Math.abs(k) * R;

// Cas particuliers : aux quartiers, l’ellipse a une largeur nulle.
// Je dessine alors un demi-disque avec une simple ligne droite.
if (rx < 0.1) {
if (phase < 0.5) {
// Premier quartier : moitié droite éclairée
return `M 0,${-R} A ${R},${R} 0 0,1 0,${R} L 0,${-R} Z`;
} else {
// Dernier quartier : moitié gauche éclairée
return `M 0,${-R} A ${R},${R} 0 0,0 0,${R} L 0,${-R} Z`;
}
}

if (phase < 0.5) {
// Phase croissante : la zone éclairée est à droite
// Je longe le limbe droit (sweep=1, sens horaire vu à l’écran)
// puis je remonte par le terminateur ; le sens dépend du signe de k
const sweep = k >= 0 ? 0 : 1;
return `M 0,${-R} A ${R},${R} 0 0,1 0,${R} A ${rx},${R} 0 0,${sweep} 0,${-R} Z`;
} else {
// Phase décroissante : la zone éclairée est à gauche
// Limbe gauche (sweep=0) puis terminateur
const sweep = k >= 0 ? 1 : 0;
return `M 0,${-R} A ${R},${R} 0 0,0 0,${R} A ${rx},${R} 0 0,${sweep} 0,${-R} Z`;
}
}

/*
═══════════════════════════════════════════════════════════════════
PARTIE 3 — DONNÉES STATIQUES (folklore et éclipses)
═══════════════════════════════════════════════════════════════════

J’ai préféré coder ces données en dur plutôt que les charger depuis un fichier
ou une API. Raisons :
1. Le projet doit rester mono-fichier
2. Le folklore est immuable
3. Les éclipses sont calculables des siècles à l’avance, donc une liste
statique pour les 2-3 prochaines années est largement suffisante.
*/

// Index 0 = janvier, 11 = décembre
// J’ai puisé dans la tradition saisonnière française et nord-américaine
// (dont les noms ont été francisés et adoptés couramment).
const FOLKLORE = [
{ name: ‘Lune du Loup’, desc: ‘Nommée d’après les loups affamés qui hurlaient au cœur de l’hiver, dans les forêts gelées.’ },
{ name: ‘Lune des Neiges’, desc: ‘Le mois où la neige tombe le plus dru, recouvrant les paysages d’un silence blanc.’ },
{ name: ‘Lune des Vers’, desc: ‘Quand la terre dégèle et que les vers réapparaissent, signe avant-coureur du printemps.’ },
{ name: ‘Lune Rose’, desc: ‘Évoquant la floraison du phlox, qui couvre les prés d’un voile rose dès les premiers jours doux.’ },
{ name: ‘Lune des Fleurs’, desc: ‘L’épanouissement général : les vergers, les haies, les jardins explosent de couleurs.’ },
{ name: ‘Lune des Fraises’, desc: ‘Mois où l’on cueille les premières fraises sauvages, parfumées et brèves.’ },
{ name: ‘Lune du Cerf’, desc: ‘Période où les bois des cerfs poussent, encore recouverts de leur velours.’ },
{ name: ‘Lune des Moissons’, desc: ‘Sa lumière dorée éclaire les champs de blé que l’on rentre à la fin de l’été.’ },
{ name: ‘Lune des Vendanges’, desc: ‘Annonce les récoltes d’automne, le raisin gorgé de soleil et les fruits mûrs.’ },
{ name: ‘Lune du Chasseur’, desc: ‘Lune basse et rousse, qui prolonge le crépuscule et accompagne les chasses d’automne.’ },
{ name: ‘Lune du Castor’, desc: ‘Quand les castors achèvent leurs huttes avant le gel, et que les pièges étaient posés.’ },
{ name: ‘Lune Froide’, desc: ‘La plus haute dans le ciel, brillant sur les longues nuits du solstice d’hiver.’ }
];

// Sources : Cité de l’espace, Stelvision, lune-pratique.fr
// Je conserve les éclipses solaires et lunaires majeures, qu’elles soient
// pleinement visibles en France ou non. Le filtrage temporel se fait au rendu.
const ECLIPSES = [
{ date: ‘2026-08-12’, type: ‘solar’, name: ‘Éclipse totale de Soleil’, desc: ‘Totale en Espagne ; partielle à 92 % à Paris, 99,5 % à Biarritz, en fin de journée.’ },
{ date: ‘2026-08-28’, type: ‘lunar’, name: ‘Éclipse partielle de Lune’, desc: ‘Visible avant l’aube depuis la France, maximum vers 06h14.’ },
{ date: ‘2027-02-06’, type: ‘solar’, name: ‘Éclipse annulaire de Soleil’, desc: ‘Visible principalement depuis l’Afrique et l’Amérique du Sud.’ },
{ date: ‘2027-02-20’, type: ‘lunar’, name: ‘Éclipse pénombrale de Lune’, desc: ‘Visible depuis la France dans la nuit du 19 au 20 février.’ },
{ date: ‘2027-08-02’, type: ‘solar’, name: ‘Éclipse totale de Soleil’, desc: ‘Une des plus longues du siècle ; partielle en France (51 % Paris, 72,5 % Toulouse).’ },
{ date: ‘2028-01-12’, type: ‘lunar’, name: ‘Éclipse partielle de Lune’, desc: ‘Visible depuis la France en seconde partie de nuit.’ },
{ date: ‘2028-01-26’, type: ‘solar’, name: ‘Éclipse annulaire de Soleil’, desc: ‘Partielle visible dans le sud-ouest de l’Europe.’ }
];

/*
═══════════════════════════════════════════════════════════════════
PARTIE 4 — FORMATAGE DES DATES EN FRANÇAIS
═══════════════════════════════════════════════════════════════════

J’aurais pu utiliser Intl.DateTimeFormat(‘fr-FR’), mais pour avoir
exactement le format que je veux (et un contrôle total des
abréviations), j’écris mes propres helpers. Coût minime, contrôle total.
*/
const FR_MONTHS = [‘janvier’,’février’,’mars’,’avril’,’mai’,’juin’,’juillet’,’août’,’septembre’,’octobre’,’novembre’,’décembre’];
const FR_DAYS = [‘dimanche’,’lundi’,’mardi’,’mercredi’,’jeudi’,’vendredi’,’samedi’];

function formatDate(d, opts = {}) {
const day = d.getDate();
const month = FR_MONTHS[d.getMonth()];
const year = d.getFullYear();
if (opts.long) {
// Format long : « mardi 28 avril 2026 »
return `${FR_DAYS[d.getDay()]} ${day} ${month} ${year}`;
}
// Format court : « 28 avril 2026 »
return `${day} ${month} ${year}`;
}

function formatTime(d) {
// Là par contre Intl.DateTimeFormat fait très bien l’affaire
return d.toLocaleTimeString(‘fr-FR’, { hour: ‘2-digit’, minute: ‘2-digit’ });
}

function relativeDays(from, to) {
const days = Math.round((to – from) / 86400000);
if (days === 0) return ‘aujourd’hui’;
if (days === 1) return ‘demain’;
return `dans ${days} jours`;
}

/*
═══════════════════════════════════════════════════════════════════
PARTIE 5 — RENDU DOM
═══════════════════════════════════════════════════════════════════
*/

// Génère une mini-lune SVG (pour les cartes « prochaines phases »)
// Mêmes calculs que la grande, juste à plus petite échelle.
function renderMiniMoon(phase, size = 56) {
const R = size / 2 – 2;
return `<svg viewBox= »${-size/2} ${-size/2} ${size} ${size} » xmlns= »http://www.w3.org/2000/svg »>
<circle cx= »0″ cy= »0″ r= »${R} » fill= »var(–moon-dark) » stroke= »rgba(201,169,97,0.3) » stroke-width= »0.5″/>
<path d= »${moonPath(phase, R)} » fill= »var(–moon-lit) »/>
</svg>`;
}

// La fonction principale. Tout est calculé puis injecté dans le DOM en une passe.
function render() {
const now = new Date();
const jd = dateToJD(now);
const phase = moonPhase(jd);
const illum = moonIllumination(phase);
const age = moonAge(jd);

// ── En-tête ──
document.getElementById(‘today-date’).textContent = formatDate(now, { long: true });

// ── Lune principale ──
// Je modifie l’attribut « d » du path SVG existant.
// Le rayon 100 correspond au viewBox -110/-110/220/220 défini dans le HTML.
document.getElementById(‘moon-lit-path’).setAttribute(‘d’, moonPath(phase, 100));

// ── Étiquettes ──
const lbl = phaseLabel(phase);
document.getElementById(‘phase-name’).textContent = lbl.name;
document.getElementById(‘phase-meta’).textContent = lbl.meta;
// u00A0 = espace insécable, pour que « 85 % » ne soit jamais coupé sur deux lignes
document.getElementById(‘illum’).textContent = Math.round(illum * 100) + ‘u00A0%’;
document.getElementById(‘age’).textContent = age.toFixed(1);
document.getElementById(‘cycle’).textContent = Math.round(phase * 100) + ‘u00A0%’;

// ── Folklore ──
// Subtilité : j’utilise le mois de la prochaine pleine lune,
// pas le mois courant. Si on est le 31 juillet et que la pleine lune
// est le 2 août, je veux afficher la « Lune des Moissons » (août),
// pas la « Lune du Cerf » (juillet) qui appartient au cycle précédent.
const nextFM = nextPhaseDate(jd, 0.5);
const folk = FOLKLORE[nextFM.getMonth()];
document.getElementById(‘folk-name’).textContent = folk.name;
document.getElementById(‘folk-desc’).textContent = folk.desc;
document.getElementById(‘folk-date’).textContent = `Prochaine pleine lune — ${formatDate(nextFM)} à ${formatTime(nextFM)}`;

// ── Prochaines phases ──
// Je calcule la date de la prochaine occurrence des 4 phases,
// puis je trie par date pour les afficher dans l’ordre chronologique.
const targets = [
{ p: 0.0, name: ‘Nouvelle lune’ },
{ p: 0.25, name: ‘Premier quartier’ },
{ p: 0.5, name: ‘Pleine lune’ },
{ p: 0.75, name: ‘Dernier quartier’ }
];
const upcoming = targets
.map(t => ({ …t, date: nextPhaseDate(jd, t.p) }))
.sort((a, b) => a.date – b.date);

// .innerHTML + template literals : pour cette taille de liste (4 cartes),
// c’est largement plus simple qu’un createElement boucle. Et il n’y a pas
// de données utilisateur ici, donc pas de risque XSS.
const grid = document.getElementById(‘phases-grid’);
grid.innerHTML = upcoming.map(p => `
<div class= »phase-card »>
<div class= »phase-icon »>${renderMiniMoon(p.p, 56)}</div>
<div class= »phase-card-name »>${p.name}</div>
<div class= »phase-card-date »>${formatDate(p.date)}</div>
<div class= »phase-card-rel »>${relativeDays(now, p.date)}</div>
</div>
`).join( »);

// ── Éclipses ──
// Je filtre celles qui sont dans le futur. À midi pour neutraliser
// les questions de fuseau horaire (l’éclipse est de toute façon notée
// au jour près dans ma source).
const list = document.getElementById(‘eclipse-list’);
const future = ECLIPSES.filter(e => new Date(e.date + ‘T12:00:00’) >= now);
list.innerHTML = future.map(e => {
const d = new Date(e.date + ‘T12:00:00’);
return `
<div class= »eclipse-item »>
<div class= »eclipse-date »>
${d.getDate()}<br>
<small>${FR_MONTHS[d.getMonth()].toUpperCase()} ${d.getFullYear()}</small>
</div>
<div>
<div class= »eclipse-name »>${e.name}</div>
<div class= »eclipse-desc »>${e.desc}</div>
</div>
<div class= »eclipse-tag ${e.type === ‘lunar’ ? ‘lunar’ :  »} »>${e.type === ‘solar’ ? ‘Soleil’ : ‘Lune’}</div>
</div>
`;
}).join( ») || ‘<p style= »color: var(–ink-muted); font-style: italic; »>Aucune éclipse répertoriée à venir.</p>’;
}

/*
═══════════════════════════════════════════════════════════════════
PARTIE 6 — DÉCLENCHEMENT
═══════════════════════════════════════════════════════════════════
*/

// Premier rendu au chargement
render();

// Rafraîchissement toutes les heures : si la page reste ouverte longtemps,
// elle continue de refléter l’heure actuelle. 3600 * 1000 = 1 h en ms.
setInterval(render, 3600 * 1000);

// Rafraîchissement quand l’utilisateur revient sur l’onglet après une absence.
// Sans ça, si on rouvre la page après 2 jours, on verrait l’état d’il y a 2 jours
// jusqu’au prochain tick du setInterval.
document.addEventListener(‘visibilitychange’, () => {
if (!document.hidden) render();
});
</script>
</body>
</html>

 

Stéphane
Stéphane
Articles: 39