◆ Portail Antalaha

Annuaire › Code ouvert

Baie d'Antalaha : diorama 3D animé jour et nuit

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/main.js · 11.2 Ko

/* ==========================================================================
 * js/main.js — POINT D'ENTRÉE : illustration, interface et boucle d'animation
 * --------------------------------------------------------------------------
 * Ce fichier est chargé en DERNIER. Il :
 *   1. crée l'Illustration Zdog (canevas net, zoom adaptatif, rotation au glissement) ;
 *   2. construit la scène (Baie.creerScene, voir scene.js) ;
 *   3. branche l'interface : heure, pause, temps qui défile, rotation lente ;
 *   4. lance la boucle d'animation avec un deltaTime.
 *
 * LA BOUCLE D'ANIMATION EN RÉSUMÉ
 *   Le navigateur appelle animer() avant chaque affichage (~60 fois/s). On
 *   mesure le temps écoulé depuis la dernière image : c'est le « deltaTime »
 *   (dt). Tout mouvement est calculé ainsi :   position += vitesse × dt
 *   Résultat : la pirogue avance à la MÊME vitesse sur un écran à 30 images/s
 *   comme sur un écran à 120 images/s.
 *
 * PERFORMANCES : dans animer(), on ne crée AUCUNE forme Zdog et presque aucun
 * objet. Les palettes de couleurs (chaînes CSS) ne sont recalculées qu'environ
 * 12 fois par seconde, et seulement si l'heure a changé.
 * ========================================================================== */

(function () {
  'use strict';

  var CFG = Baie.CONFIG;

  // ---- 1. ÉTAT DE L'APPLICATION -------------------------------------------
  var etat = {
    heure: CFG.heureDepart,   // heure de la journée affichée (0 à 24)
    tempsAuto: true,          // le temps s'écoule-t-il tout seul ?
    pause: false,             // animation en pause ?
    rotationLente: false,     // la vue tourne-t-elle doucement ?
    glisse: false,            // l'utilisateur est-il en train de glisser sur le canevas ?
    temps: 0,                 // temps d'animation cumulé (s) ; s'arrête en pause
    dernier: 0,               // horodatage de l'image précédente (ms)
    chronoPalette: 0,         // temps réel écoulé depuis le dernier recalcul de palette
    heurePalette: -1,         // heure pour laquelle la palette a été calculée
    fps: 60,                  // images par seconde, lissées
    chronoFps: 0,             // pour n'afficher les FPS que 2 fois par seconde
  };

  // Respect des préférences d'accessibilité : « réduire les animations ».
  try {
    if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
      etat.tempsAuto = false;
    }
  } catch (e) { /* on garde les valeurs par défaut */ }

  // ---- 2. L'ILLUSTRATION ---------------------------------------------------
  /**
   * Zoom adaptatif : toute la scène (largeurScene × hauteurScene unités)
   * doit tenir dans le canevas, qu'il soit large (ordinateur) ou étroit
   * (téléphone). On prend le plus petit des deux rapports.
   * @param {number} largeur Largeur CSS du canevas (pixels).
   * @param {number} hauteur Hauteur CSS du canevas (pixels).
   * @returns {number} Le zoom à appliquer.
   */
  function calculerZoom(largeur, hauteur) {
    return Math.min(largeur / CFG.largeurScene, hauteur / CFG.hauteurScene);
  }

  var illo = new Zdog.Illustration({
    element: '#baie-canvas',
    dragRotate: true,      // le doigt / la souris fait tourner le diorama
    resize: true,          // le canevas suit sa boîte CSS. Zdog multiplie sa résolution
                           // par window.devicePixelRatio : rendu net sur écrans denses.
    rotate: { x: CFG.vueDepart.x, y: CFG.vueDepart.y },
    onDragStart: function () { etat.glisse = true; },
    onDragEnd: function () { etat.glisse = false; },
    // Appelée à chaque redimensionnement (rotation du téléphone, fenêtre…).
    // `this` est l'illustration.
    onResize: function (largeur, hauteur) { this.zoom = calculerZoom(largeur, hauteur); },
  });
  illo.zoom = calculerZoom(illo.width, illo.height);

  // ---- 3. LA SCÈNE ---------------------------------------------------------
  var scene = Baie.creerScene(illo);

  // ---- 4. RÉFÉRENCES VERS L'INTERFACE --------------------------------------
  function el(id) { return document.getElementById(id); }

  var zoneScene = el('scene-baie');          // la boîte du canevas (porte le dégradé de ciel)
  var curseurHeure = el('ctrl-heure');
  var texteHeure = el('txt-heure');
  var textePhase = el('txt-phase');
  var iconeAstre = el('ico-astre');
  var caseTemps = el('chk-temps');
  var caseRotation = el('chk-rotation');
  var boutonPause = el('btn-pause');
  var iconePause = el('ico-pause');
  var libellePause = el('lib-pause');
  var ruban = el('ruban');
  var repere = el('ruban-repere');
  var texteFps = el('info-fps');

  // ---- 5. LA PALETTE -------------------------------------------------------
  // Objet créé UNE FOIS et réutilisé à chaque calcul (voir palette.js).
  var palette = Baie.Palette.creerSortie();

  /**
   * Formate une heure décimale en texte lisible : 14.5 -> « 14 h 30 ».
   * @param {number} h Heure décimale.
   * @returns {string}
   */
  function formaterHeure(h) {
    var heures = Math.floor(h);
    var minutes = Math.floor((h - heures) * 60);
    return heures + ' h ' + (minutes < 10 ? '0' : '') + minutes;
  }

  /**
   * Recalcule la palette pour l'heure courante et l'applique PARTOUT :
   * formes 3D, fond CSS du ciel, textes de l'interface.
   * Appelée environ 12 fois par seconde au maximum (voir animer()).
   */
  function majPalette() {
    Baie.Palette.calculer(etat.heure, palette);
    scene.appliquerPalette(palette);
    etat.heurePalette = etat.heure;
    etat.chronoPalette = 0;

    // Le dégradé du ciel : du CSS appliqué derrière le canevas transparent.
    if (zoneScene) {
      zoneScene.style.background =
        'linear-gradient(180deg, ' + palette.css.cielHaut + ' 0%, ' + palette.css.cielBas + ' 100%)';
    }

    // Textes et repères de l'interface.
    var texte = formaterHeure(etat.heure);
    if (texteHeure) { texteHeure.textContent = texte; }
    if (textePhase) { textePhase.textContent = palette.phase; }
    if (curseurHeure) {
      curseurHeure.value = etat.heure.toFixed(2);
      curseurHeure.setAttribute('aria-valuetext', texte + ', ' + palette.phase);
    }
    if (repere) { repere.style.left = (etat.heure / 24 * 100).toFixed(2) + '%'; }
    if (iconeAstre) {
      var nuit = palette.etoiles > 0.5;
      iconeAstre.src = nuit ? 'medias/lune.svg' : 'medias/soleil.svg';
    }
  }

  /**
   * Change l'heure à la main (curseur ou ruban) : on coupe le défilement
   * automatique et on met l'affichage à jour tout de suite.
   * @param {number} heure Nouvelle heure.
   */
  function choisirHeure(heure) {
    etat.heure = Zdog.modulo(heure, 24);
    etat.tempsAuto = false;
    if (caseTemps) { caseTemps.checked = false; }
    majPalette();
  }

  // ---- 6. ÉVÉNEMENTS DE L'INTERFACE ----------------------------------------
  if (curseurHeure) {
    curseurHeure.min = '0';
    curseurHeure.max = '24';
    curseurHeure.addEventListener('input', function () {
      choisirHeure(parseFloat(curseurHeure.value));   // .value est du texte : on convertit
    });
  }

  if (caseTemps) {
    caseTemps.checked = etat.tempsAuto;
    caseTemps.addEventListener('change', function () { etat.tempsAuto = caseTemps.checked; });
  }

  if (caseRotation) {
    caseRotation.addEventListener('change', function () { etat.rotationLente = caseRotation.checked; });
  }

  if (boutonPause) {
    boutonPause.addEventListener('click', function () {
      etat.pause = !etat.pause;
      boutonPause.setAttribute('aria-pressed', etat.pause ? 'true' : 'false');
      if (libellePause) { libellePause.textContent = etat.pause ? 'Reprendre' : 'Pause'; }
      if (iconePause) { iconePause.src = etat.pause ? 'medias/lecture.svg' : 'medias/pause.svg'; }
    });
  }

  // Bouton « Vue de départ » : remet la caméra dans sa position initiale.
  var boutonVue = el('btn-vue');
  if (boutonVue) {
    boutonVue.addEventListener('click', function () {
      illo.rotate.set({ x: CFG.vueDepart.x, y: CFG.vueDepart.y, z: 0 });
    });
  }

  // ---- 7. Zdog.Dragger : GLISSER SUR LE RUBAN DU TEMPS ---------------------
  // Dragger est la classe de Zdog qui suit un glissement (souris ou doigt).
  // `Illustration` en hérite (c'est ce qui permet dragRotate). Ici on l'utilise
  // SEUL, sur un autre élément : le « ruban » coloré qui représente les 24 h.
  /**
   * Convertit la position horizontale d'un pointeur en heure (0 à 24).
   * @param {{pageX:number}} pointeur Événement ou toucher fourni par Dragger.
   * @returns {number} L'heure correspondante.
   */
  function heureDepuisPointeur(pointeur) {
    var boite = ruban.getBoundingClientRect();
    var x = pointeur.pageX - (window.pageXOffset || 0) - boite.left;   // position dans le ruban
    return Baie.borner(x / (boite.width || 1), 0, 1) * 24;
  }

  if (ruban) {
    new Zdog.Dragger({
      startElement: ruban,
      onDragStart: function (pointeur) { choisirHeure(heureDepuisPointeur(pointeur)); },
      onDragMove: function (pointeur) { choisirHeure(heureDepuisPointeur(pointeur)); },
    });
  }

  // ---- 8. LA BOUCLE D'ANIMATION --------------------------------------------
  /**
   * Appelée par le navigateur avant chaque affichage.
   * @param {number} maintenant Horodatage en millisecondes (fourni par requestAnimationFrame).
   */
  function animer(maintenant) {
    // a) deltaTime en SECONDES, borné entre 0 et deltaMax (0,1 s) : si l'onglet
    //    est resté caché, on évite un énorme saut au retour.
    var dtReel = Baie.borner((maintenant - etat.dernier) / 1000, 0, CFG.deltaMax);
    etat.dernier = maintenant;

    // b) En pause, le temps d'animation est figé (dt = 0) mais on continue de
    //    dessiner : on peut toujours tourner la vue avec le doigt.
    var dt = etat.pause ? 0 : dtReel;
    etat.temps += dt;

    // c) Le temps de la journée avance : 24 h en `dureeJournee` secondes.
    if (etat.tempsAuto) {
      etat.heure = Zdog.modulo(etat.heure + (dt * 24) / CFG.dureeJournee, 24);
    }

    // d) Palette : recalculée au plus ~12 fois par seconde, et seulement
    //    si l'heure a changé (les chaînes de couleurs ne sont pas gratuites).
    etat.chronoPalette += dtReel;
    if (etat.heure !== etat.heurePalette && etat.chronoPalette > 0.08) { majPalette(); }

    // e) Caméra : rotation lente optionnelle, et inclinaison toujours bornée.
    if (etat.rotationLente && !etat.glisse) { illo.rotate.y += CFG.rotationLente * dtReel; }
    illo.rotate.x = Baie.borner(illo.rotate.x, CFG.inclinaisonMin, CFG.inclinaisonMax);

    // f) On fait avancer la scène, puis on la dessine.
    scene.mettreAJour(etat.temps, dt, etat.heure);
    illo.updateRenderGraph();

    // g) Compteur d'images par seconde (moyenne glissante), affiché 2 fois/s.
    if (dtReel > 0) { etat.fps = etat.fps * 0.92 + (1 / dtReel) * 0.08; }
    etat.chronoFps += dtReel;
    if (texteFps && etat.chronoFps > 0.5) {
      texteFps.textContent = Math.round(etat.fps) + ' images/s';
      etat.chronoFps = 0;
    }

    requestAnimationFrame(animer);
  }

  // ---- 9. DÉMARRAGE --------------------------------------------------------
  majPalette();                    // première palette AVANT la première image
  requestAnimationFrame(animer);

  // Pour les curieux : la scène est accessible depuis la console du navigateur,
  // par exemple  Baie.debug.illo.rotate.y = 1;
  Baie.debug = { illo: illo, scene: scene, etat: etat };
})();