◆ 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/pirogue.js · 9.5 Ko

/* ==========================================================================
 * js/pirogue.js — LA PIROGUE À BALANCIER (lakana)
 * --------------------------------------------------------------------------
 * Baie.creerPirogue(parent, mer) construit une pirogue malgache et la fait
 * naviguer en rond autour de l'île, en flottant sur les vagues.
 *
 * Hiérarchie des ancres (c'est la clé du mouvement) :
 *
 *   racine      position sur la mer + direction du cap (rotate.y)
 *    └─ coque   tangage et roulis (rotate.z / rotate.x), pour suivre la houle
 *        ├─ la coque, le pêcheur, le mât et la voile
 *        └─ le balancier : deux traverses + un flotteur latéral
 *
 * Pourquoi deux niveaux ? Les rotations de Zdog s'appliquent dans l'ordre
 * z, puis y, puis x. Séparer le cap (racine) et la houle (coque) évite que
 * le tangage ne se fasse autour du mauvais axe quand le bateau tourne.
 *
 * Dans le repère de la pirogue : x = avant/arrière (l'avant est à +x),
 * y = haut (négatif), z = côté. Le balancier est sur le côté +z.
 *
 * SILLAGE : on utilise un « POOL » de 8 petites sphères créées UNE FOIS et
 * réutilisées en boucle (un tampon circulaire). On ne crée jamais de forme
 * pendant l'animation : c'est une bonne pratique de performance.
 *
 * Dépend de : Zdog, Baie.Palette (palette.js) ; reçoit `mer` (mer.js).
 * ========================================================================== */

(function () {
  'use strict';

  var TAU = Zdog.TAU;

  // ---- Constantes réglables -------------------------------------------------
  var RAYON_X = 112;            // largeur de la trajectoire (ellipse autour de l'île)
  var RAYON_Z = 92;             // profondeur de la trajectoire
  var VITESSE_ANGULAIRE = 0.22; // radians par seconde : un tour en ~28 s
  var NB_SILLAGE = 8;           // nombre de sphères du sillage
  var INTERVALLE_SILLAGE = 0.35;// secondes entre deux « gouttes »
  var VIE_SILLAGE = 2.8;        // durée de vie d'une goutte, en secondes

  var BOIS = '#a0522d';         // coque
  var BOIS_CLAIR = '#d29a62';   // bordé et flotteur
  var BOIS_FONCE = '#6b4423';   // traverses et mât
  var TOILE = '#f4ead2';        // voile

  /**
   * Construit la pirogue et son sillage.
   *
   * @param {Zdog.Anchor} parent  Où l'accrocher (le « monde »).
   * @param {Object} mer          Objet renvoyé par Baie.creerMer() (pour mer.hauteur).
   * @returns {Object} { racine, mettreAJour(t, dt), appliquerPalette(p) }
   */
  function creerPirogue(parent, mer) {

    // --- Les deux ancres de base ---
    var racine = new Zdog.Anchor({ addTo: parent });
    var coque = new Zdog.Anchor({ addTo: racine });

    // 1) LA COQUE ----------------------------------------------------------
    // Une courbe de Bézier épaisse : de l'arrière (-30) à l'avant (+30), en
    // creux au milieu. Avec un trait de 8, elle ressemble à un tronc creusé.
    new Zdog.Shape({
      addTo: coque,
      path: [
        { x: -30, y: -7 },
        { bezier: [{ x: -18, y: 2.5 }, { x: 18, y: 2.5 }, { x: 30, y: -7 }] },
      ],
      closed: false,
      stroke: 8,
      color: BOIS,
    });

    // Le bordé : une ligne plus fine et plus claire, juste au-dessus.
    new Zdog.Shape({
      addTo: coque,
      path: [
        { x: -31, y: -10 },
        { bezier: [{ x: -18, y: -1 }, { x: 18, y: -1 }, { x: 31, y: -10 }] },
      ],
      closed: false,
      stroke: 2.5,
      color: BOIS_CLAIR,
    });

    // 2) LE PÊCHEUR --------------------------------------------------------
    // Corps : un trait vertical épais ; tête : une sphère ; chapeau : un cône.
    new Zdog.Shape({
      addTo: coque,
      path: [{ y: -3 }, { y: -15 }],
      closed: false,
      stroke: 5,
      translate: { x: -8 },
      color: '#e0573b',                 // chemise rouge
    });
    new Zdog.Shape({                    // la tête (un point épais = une sphère)
      addTo: coque,
      stroke: 5.5,
      translate: { x: -8, y: -20 },
      color: '#8c5a3c',
    });
    new Zdog.Cone({                     // le chapeau pointu (salakova)
      addTo: coque,
      diameter: 13,
      length: 4,
      translate: { x: -8, y: -22.5 },
      rotate: { x: TAU / 4 },           // la pointe du cône regarde vers le haut
      stroke: false,
      color: '#d9b25f',
      backface: '#b8913f',
    });

    // 3) LE MÂT ET LA VOILE ------------------------------------------------
    var xMat = 6;
    new Zdog.Shape({
      addTo: coque,
      path: [{ y: -6 }, { y: -44 }],
      closed: false,
      stroke: 2,
      translate: { x: xMat },
      color: BOIS_FONCE,
    });

    // La voile est dans une ancre à part, placée au pied du mât, pour pouvoir
    // la faire pivoter doucement dans le vent (voile.rotate.y).
    var voile = new Zdog.Anchor({ addTo: coque, translate: { x: xMat, y: -6 } });
    var toile = new Zdog.Shape({
      addTo: voile,
      path: [
        { x: 0, y: -37 },       // haut du mât
        { x: 0, y: -2 },        // bas du mât
        { x: -22, y: -2 },      // bout de la bôme, vers l'arrière
      ],
      fill: true,
      stroke: 1.5,
      color: TOILE,
    });

    // 4) LE BALANCIER ------------------------------------------------------
    // Deux traverses (lignes de la coque vers le côté, axe z) et le flotteur
    // (une longue courbe parallèle à la coque, posée sur l'eau).
    [-12, 12].forEach(function (x) {
      new Zdog.Shape({
        addTo: coque,
        path: [{ x: x, y: -6, z: 0 }, { x: x, y: -2, z: 24 }],
        closed: false,
        stroke: 1.8,
        color: BOIS_FONCE,
      });
    });
    new Zdog.Shape({
      addTo: coque,
      path: [
        { x: -26, y: -3, z: 24 },
        { bezier: [{ x: -14, y: 1.5, z: 24 }, { x: 14, y: 1.5, z: 24 }, { x: 26, y: -3, z: 24 }] },
      ],
      closed: false,
      stroke: 5,
      color: BOIS_CLAIR,
    });

    // 5) LE SILLAGE (pool de sphères réutilisées) --------------------------
    // Créées UNE fois, hors de la boucle d'animation.
    var gouttes = [];          // les formes
    var gx = [], gz = [], gAge = [];   // position et âge de chaque goutte
    for (var i = 0; i < NB_SILLAGE; i++) {
      gouttes.push(new Zdog.Shape({
        addTo: parent,
        stroke: 5,
        color: '#ffffff',
        visible: false,
      }));
      gx.push(0); gz.push(0);
      gAge.push(VIE_SILLAGE);   // « déjà vieille » : invisible au départ
    }
    var prochaine = 0;          // index de la prochaine goutte à (ré)utiliser
    var chrono = 0;             // temps écoulé depuis la dernière goutte

    // --- État de la trajectoire ---
    var angle = 0.6;            // position sur l'ellipse (radians)

    /**
     * Fait avancer la pirogue. À appeler à chaque image.
     *
     * @param {number} t   Temps écoulé total (secondes) : sert à la houle.
     * @param {number} dt  Temps depuis l'image précédente (secondes) : sert au déplacement.
     *                     Quand l'animation est en pause, dt vaut 0 : la pirogue s'arrête.
     */
    function mettreAJour(t, dt) {
      // a) Avancer sur l'ellipse. Vitesse × dt = indépendant du nombre d'images/s.
      angle += VITESSE_ANGULAIRE * dt;
      var x = RAYON_X * Math.cos(angle);
      var z = RAYON_Z * Math.sin(angle);

      // b) Le cap : la direction du mouvement est la DÉRIVÉE de la position.
      //    d(x)/d(angle) = -RAYON_X·sin ; d(z)/d(angle) = RAYON_Z·cos.
      var dx = -RAYON_X * Math.sin(angle);
      var dz = RAYON_Z * Math.cos(angle);
      // Un objet tourné de θ autour de y voit son axe +x pointer vers
      // (cos θ, 0, -sin θ). On cherche θ tel que ce soit (dx, dz) : θ = atan2(-dz, dx).
      racine.rotate.y = Math.atan2(-dz, dx);

      // c) Flotter : la hauteur de l'eau sous la pirogue (fonction de mer.js).
      racine.translate.x = x;
      racine.translate.z = z;
      racine.translate.y = mer.hauteur(x, z, t) - 2;

      // d) Tangage (avant/arrière, autour de z) et roulis (côté, autour de x).
      coque.rotate.z = 0.07 * Math.sin(t * 1.6);
      coque.rotate.x = 0.06 * Math.sin(t * 1.3 + 1);

      // e) La voile pivote doucement dans le vent.
      voile.rotate.y = 0.18 * Math.sin(t * 0.8);

      // f) Le sillage : on pose une goutte à intervalle régulier…
      chrono += dt;
      if (chrono >= INTERVALLE_SILLAGE) {
        chrono = 0;
        gx[prochaine] = x;
        gz[prochaine] = z;
        gAge[prochaine] = 0;                         // elle redevient « jeune »
        prochaine = (prochaine + 1) % NB_SILLAGE;    // tampon circulaire : 7 -> 0
      }
      // …et on vieillit toutes les gouttes (elles rétrécissent puis disparaissent).
      for (var i = 0; i < NB_SILLAGE; i++) {
        gAge[i] += dt;
        var vivante = gAge[i] < VIE_SILLAGE;
        var g = gouttes[i];
        g.visible = vivante;
        if (vivante) {
          g.translate.x = gx[i];
          g.translate.z = gz[i];
          g.translate.y = mer.hauteur(gx[i], gz[i], t) - 2;
          g.stroke = 5 * (1 - gAge[i] / VIE_SILLAGE);
        }
      }
    }

    var tmp = [0, 0, 0];
    var rgbToile = Baie.Palette.hexVersRgb(TOILE);

    /**
     * Adapte la voile et le sillage à l'ambiance (plus sombres la nuit).
     * @param {Object} p Palette (Baie.Palette.calculer()).
     */
    function appliquerPalette(p) {
      toile.color = Baie.Palette.nuancer(rgbToile, -0.55 * p.etoiles, tmp);
      var couleurEcume = p.css.ecume;
      for (var i = 0; i < gouttes.length; i++) { gouttes[i].color = couleurEcume; }
    }

    return { racine: racine, mettreAJour: mettreAJour, appliquerPalette: appliquerPalette };
  }

  Baie.creerPirogue = creerPirogue;
})();