◆ Portail Antalaha

Annuaire › Code ouvert

Horloge d'Antalaha - décor jour et nuit (p5.js)

tutorial · Voir le site ↗
Connectez-vous pour copier ce site

Code publié par son auteur, en lecture seule. « Copier dans mon espace » crée un nouveau site à vous (non publié) que vous pouvez modifier librement.

js/decor.js · 13.2 Ko

/* ============================================================
   decor.js - le paysage qui change selon l'heure
   ============================================================
   Ce fichier ne contient que des FONCTIONS DE DESSIN (aucun
   setup/draw). C'est sketch.js qui les appelle, dans l'ordre :
   ciel -> étoiles -> astre -> mer -> colline -> palmiers.
   On dessine "du fond vers l'avant" : ce qui est dessiné après
   se superpose à ce qui est dessiné avant (comme des calques).

   RAPPEL : système de coordonnées de p5
   - l'origine (0, 0) est en HAUT à GAUCHE du canevas ;
   - x augmente vers la DROITE ;
   - y augmente vers le BAS (attention : à l'envers des maths !).
   ============================================================ */

/* ------------------------------------------------------------
   preparerEtapes()
   But : convertir les couleurs hexadécimales de ETAPES_BRUTES en
         objets couleur p5 utilisables avec lerpColor().
   Retour : un tableau d'étapes {h, haut, bas, mer, etoiles}.
   À appeler UNE fois, dans setup().
   ------------------------------------------------------------ */
function preparerEtapes() {
  // .map() fabrique un nouveau tableau en transformant chaque élément.
  return ETAPES_BRUTES.map(function (e) {
    return {
      h: e.h,
      haut: color(e.haut),   // color('#rrggbb') -> objet couleur p5
      bas: color(e.bas),
      mer: color(e.mer),
      etoiles: e.etoiles
    };
  });
}

/* ------------------------------------------------------------
   creerEtoiles(n)
   But : fabriquer n étoiles à des positions aléatoires.
   On stocke les positions en FRACTIONS (0 à 1) de la taille du
   canevas : ainsi elles restent bien placées si l'écran change
   de taille.
   Paramètre : n = nombre d'étoiles.
   Retour : tableau d'objets {x, y, taille, phase}.
   ------------------------------------------------------------ */
function creerEtoiles(n) {
  const liste = [];
  for (let i = 0; i < n; i++) {
    liste.push({
      x: random(),                       // random() sans argument : entre 0 et 1
      y: random(POS_HORIZON * 0.95),     // seulement dans le ciel (au-dessus de l'horizon)
      taille: random(1, 2.6),
      phase: random(TWO_PI)              // décalage pour que les étoiles ne scintillent pas toutes ensemble
    });
  }
  return liste;
}

/* ------------------------------------------------------------
   couleursPourHeure(heureDecimale, etapes)
   But : trouver les couleurs du ciel à une heure donnée.
   C'est ici qu'on utilise DEUX fonctions importantes de p5 :
     - map()       : convertit une valeur d'un intervalle à un autre ;
     - lerpColor() : mélange deux couleurs (0 = la 1re, 1 = la 2e).
   Paramètres :
     heureDecimale : ex. 14.5 pour 14 h 30
     etapes        : tableau préparé par preparerEtapes()
   Retour : {haut, bas, mer, etoiles}
   ------------------------------------------------------------ */
function couleursPourHeure(heureDecimale, etapes) {
  // On cherche les deux étapes qui encadrent l'heure actuelle.
  let i = 0;
  while (i < etapes.length - 2 && heureDecimale >= etapes[i + 1].h) {
    i++;
  }
  const a = etapes[i];       // étape juste avant
  const b = etapes[i + 1];   // étape juste après

  // map(valeur, minAvant, maxAvant, minAprès, maxAprès)
  // Exemple : à 7,5 h, entre a.h = 6,5 et b.h = 8,5, on obtient 0,5
  // (on est à mi-chemin). constrain() garantit un résultat entre 0 et 1.
  const t = constrain(map(heureDecimale, a.h, b.h, 0, 1), 0, 1);

  return {
    haut: lerpColor(a.haut, b.haut, t),
    bas: lerpColor(a.bas, b.bas, t),
    mer: lerpColor(a.mer, b.mer, t),
    etoiles: lerp(a.etoiles, b.etoiles, t)   // lerp() = même idée pour de simples nombres
  };
}

/* ------------------------------------------------------------
   nomDeLaPhase(heureDecimale)
   But : donner le nom (« Aube », « Jour »...) de l'heure actuelle.
   Retour : une chaîne de caractères.
   ------------------------------------------------------------ */
function nomDeLaPhase(heureDecimale) {
  let nom = PHASES[0].nom;
  for (const p of PHASES) {
    if (heureDecimale >= p.de) nom = p.nom;   // la dernière phase dépassée gagne
  }
  return nom;
}

/* ------------------------------------------------------------
   dessinerCiel(w, hauteurCiel, couleurs)
   But : dessiner le dégradé vertical du ciel.
   p5 n'a pas de "dégradé" tout fait : on dessine donc une ligne
   horizontale par pixel de hauteur, chacune d'une couleur un peu
   différente (lerpColor entre le haut et le bas).
   Paramètres : w = largeur, hauteurCiel = hauteur de la zone,
                couleurs = résultat de couleursPourHeure().
   ------------------------------------------------------------ */
function dessinerCiel(w, hauteurCiel, couleurs) {
  strokeWeight(1);
  for (let y = 0; y <= hauteurCiel; y++) {
    const t = y / hauteurCiel;                      // 0 en haut -> 1 à l'horizon
    stroke(lerpColor(couleurs.haut, couleurs.bas, t));
    line(0, y, w, y);                               // line(x1, y1, x2, y2)
  }
}

/* ------------------------------------------------------------
   dessinerEtoiles(etoiles, w, hauteurCiel, visibilite)
   But : dessiner les étoiles qui scintillent la nuit.
   Paramètres :
     etoiles    : tableau créé par creerEtoiles()
     visibilite : 0 à 1 (venant de l'étape en cours)
   ------------------------------------------------------------ */
function dessinerEtoiles(etoiles, w, hauteurCiel, visibilite) {
  if (visibilite <= 0.01) return;   // de jour : rien à dessiner, on sort de la fonction
  noStroke();
  for (const e of etoiles) {
    // sin() oscille entre -1 et 1 : on s'en sert pour faire "respirer" la luminosité.
    // millis() = nombre de millisecondes écoulées depuis le début du programme.
    const scintillement = 0.65 + 0.35 * sin(millis() / 500 + e.phase);
    fill(255, 255, 240, 255 * visibilite * scintillement);   // fill(r, g, b, transparence)
    circle(e.x * w, e.y * hauteurCiel / POS_HORIZON, e.taille);
  }
}

/* ------------------------------------------------------------
   positionAstre(t, w, yHorizon)
   But : calculer où se trouve le soleil ou la lune.
   L'astre suit un ARC de cercle : il se lève à gauche, monte, puis
   se couche à droite.
   Paramètres :
     t        : progression du trajet, de 0 (lever) à 1 (coucher)
     w        : largeur du canevas
     yHorizon : hauteur (y) de l'horizon
   Retour : {x, y}
   ------------------------------------------------------------ */
function positionAstre(t, w, yHorizon) {
  const marge = w * 0.08;
  const x = map(t, 0, 1, marge, w - marge);   // glisse de gauche à droite
  const hauteurArc = yHorizon * 0.8;
  // sin(t * PI) vaut 0 aux deux extrémités et 1 au milieu : parfait pour un arc.
  // On SOUSTRAIT car y augmente vers le bas.
  const y = yHorizon - sin(t * PI) * hauteurArc;
  return { x: x, y: y };
}

/* ------------------------------------------------------------
   dessinerSoleilEtLune(heureDecimale, w, yHorizon)
   But : afficher le soleil (6 h -> 18 h) ou la lune (18 h -> 6 h).
   Le soleil est derrière l'horizon la nuit ; on dessine donc la
   mer et la colline PAR-DESSUS l'astre pour le "cacher".
   ------------------------------------------------------------ */
function dessinerSoleilEtLune(heureDecimale, w, yHorizon) {
  noStroke();
  const rayon = w * 0.05;

  if (heureDecimale >= 6 && heureDecimale < 18) {
    // --- SOLEIL ---
    const t = (heureDecimale - 6) / 12;              // 0 à 6 h, 1 à 18 h
    const p = positionAstre(t, w, yHorizon);
    // Plus le soleil est haut, plus il est jaune pâle.
    const c = lerpColor(color(COULEUR_SOLEIL_BAS), color(COULEUR_SOLEIL_HAUT), sin(t * PI));
    // Halo : plusieurs cercles transparents de plus en plus grands.
    for (let k = 4; k >= 1; k--) {
      fill(red(c), green(c), blue(c), 28);
      circle(p.x, p.y, rayon * 2 + k * rayon * 0.9);
    }
    fill(c);
    circle(p.x, p.y, rayon * 2);
  } else {
    // --- LUNE ---
    // On "décale" l'heure pour que la lune se lève à 18 h : (h + 6) modulo 24.
    const hn = (heureDecimale + 6) % 24;             // 18 h -> 0 ; 0 h -> 6 ; 6 h -> 12
    const t = hn / 12;
    const p = positionAstre(t, w, yHorizon);
    fill(red(color(COULEUR_LUNE)), green(color(COULEUR_LUNE)), blue(color(COULEUR_LUNE)), 40);
    circle(p.x, p.y, rayon * 3.2);                   // halo
    fill(COULEUR_LUNE);
    circle(p.x, p.y, rayon * 1.7);
    // Croissant : un disque de la couleur du ciel qui "mord" la lune.
    fill(red(color('#0d1636')), green(color('#0d1636')), blue(color('#0d1636')), 180);
    circle(p.x + rayon * 0.5, p.y - rayon * 0.2, rayon * 1.5);
  }
}

/* ------------------------------------------------------------
   dessinerMer(w, h, yHorizon, couleurs, heureDecimale)
   But : dessiner la mer avec de petits reflets animés.
   ------------------------------------------------------------ */
function dessinerMer(w, h, yHorizon, couleurs, heureDecimale) {
  // Dégradé : la mer est plus claire à l'horizon, plus sombre au bord bas.
  const sombre = lerpColor(couleurs.mer, color(0, 20, 40), 0.55);
  strokeWeight(1);
  for (let y = yHorizon; y <= h; y++) {
    stroke(lerpColor(couleurs.mer, sombre, (y - yHorizon) / (h - yHorizon)));
    line(0, y, w, y);
  }

  // Petits reflets : des traits clairs qui ondulent avec le temps.
  const nuit = couleurs.etoiles;                       // 0 de jour, 1 la nuit
  strokeWeight(max(1, w * 0.004));
  for (let i = 0; i < 14; i++) {
    const y = yHorizon + (h - yHorizon) * (0.08 + i * 0.065);
    const decalage = sin(millis() / 900 + i * 1.7) * w * 0.03;
    const x = w * (0.12 + ((i * 0.37) % 0.76)) + decalage;
    const longueur = w * (0.05 + 0.002 * i * 4);
    stroke(255, 255, 255, lerp(70, 35, nuit));
    line(x - longueur / 2, y, x + longueur / 2, y);
  }
}

/* ------------------------------------------------------------
   dessinerColline(w, yHorizon, couleurs)
   But : dessiner une silhouette de colline à droite (comme celles
   qui entourent la baie d'Antalaha).
   On utilise beginShape()/vertex()/endShape() pour tracer un
   polygone point par point.
   ------------------------------------------------------------ */
function dessinerColline(w, yHorizon, couleurs) {
  // Silhouette = couleur de la mer très assombrie.
  const c = lerpColor(couleurs.mer, color(5, 15, 15), 0.7);
  noStroke();
  fill(c);
  beginShape();
  vertex(w * 0.45, yHorizon);
  for (let x = w * 0.45; x <= w; x += w / 40) {
    // map() : x va de 0,45 w à w ; on le convertit en angle de 0 à PI
    // pour obtenir une bosse douce grâce à sin().
    const a = map(x, w * 0.45, w, 0, PI);
    vertex(x, yHorizon - sin(a) * w * 0.13 - sin(a * 3) * w * 0.012);
  }
  vertex(w, yHorizon);
  endShape(CLOSE);
}

/* ------------------------------------------------------------
   dessinerPalmier(x, ySol, hauteur, couleur)
   But : dessiner un palmier simplifié (tronc courbe + palmes).
   Ici on utilise push() / pop() / translate() / rotate() :
     - push() sauvegarde l'état de dessin (position, rotation...) ;
     - translate(x, y) déplace l'origine (0, 0) à cet endroit ;
     - rotate(angle) tourne le repère (en RADIANS) ;
     - pop() restaure l'état sauvegardé.
   Ainsi, on dessine les palmes "autour de (0, 0)" sans se soucier
   de leur place réelle.
   Paramètres : x, ySol = pied du tronc ; hauteur ; couleur = p5 color
   ------------------------------------------------------------ */
function dessinerPalmier(x, ySol, hauteur, couleur) {
  const sommetX = x + hauteur * 0.18;         // le tronc penche un peu vers la droite
  const sommetY = ySol - hauteur;

  // Tronc : une courbe de Bézier (2 points + 2 points de contrôle).
  noFill();
  stroke(couleur);
  strokeWeight(max(2, hauteur * 0.05));
  bezier(x, ySol, x - hauteur * 0.05, ySol - hauteur * 0.4,
         x + hauteur * 0.05, ySol - hauteur * 0.7, sommetX, sommetY);

  // Palmes : 7 courbes qui partent du sommet, chacune tournée différemment.
  push();
  translate(sommetX, sommetY);                // le sommet devient l'origine
  strokeWeight(max(1.5, hauteur * 0.035));
  for (let i = 0; i < 7; i++) {
    push();
    rotate(map(i, 0, 6, -PI * 0.95, PI * 0.05));   // étale les palmes en éventail
    bezier(0, 0, hauteur * 0.25, -hauteur * 0.12,
           hauteur * 0.45, -hauteur * 0.02, hauteur * 0.5, hauteur * 0.22);
    pop();
  }
  pop();                                      // on revient au repère normal
}

/* ------------------------------------------------------------
   dessinerDecor(heureDecimale, w, h, couleurs, etoiles)
   But : assembler tout le paysage, dans le bon ordre.
   C'est la seule fonction de ce fichier appelée par draw().
   ------------------------------------------------------------ */
function dessinerDecor(heureDecimale, w, h, couleurs, etoiles) {
  const yHorizon = h * POS_HORIZON;

  dessinerCiel(w, yHorizon, couleurs);
  dessinerEtoiles(etoiles, w, yHorizon, couleurs.etoiles);
  dessinerSoleilEtLune(heureDecimale, w, yHorizon);
  dessinerMer(w, h, yHorizon, couleurs, heureDecimale);
  dessinerColline(w, yHorizon, couleurs);

  // Les palmiers sont des silhouettes sombres sur le côté gauche.
  const silhouette = lerpColor(couleurs.mer, color(5, 15, 15), 0.75);
  dessinerPalmier(w * 0.06, h * 0.97, h * 0.42, silhouette);
  dessinerPalmier(w * 0.16, h * 1.0, h * 0.3, silhouette);
}