◆ Portail Antalaha

Annuaire › Code ouvert

Maison 3D pour une agence immobilière

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 · 9.1 Ko

/* ==========================================================================
 * js/main.js — ASSEMBLAGE : scène, interface et boucle d'animation
 * --------------------------------------------------------------------------
 * Ce fichier est le « chef d'orchestre » :
 *   1. crée l'Illustration Zdog (adaptée à la taille de l'écran) ;
 *   2. assemble le monde : sol, maison, véranda, escalier, arbre ;
 *   3. branche l'interface : boutons de vue, zoom, couleur de façade ;
 *   4. lance la boucle d'animation.
 *
 * Ordre des scripts (voir index.html) :
 *   zdog.min.js, config.js, outils.js, sol.js, maison.js, veranda.js,
 *   arbre.js, camera.js, main.js
 * ========================================================================== */

(function () {
  'use strict';

  var CAM = Maison.CAMERA;

  // ---- 1. ÉTAT ------------------------------------------------------------
  var etat = {
    zoomBase: 1,          // zoom calculé selon la taille du canevas
    zoomUtilisateur: 1,   // multiplicateur choisi avec le curseur
    dernierTemps: 0,
  };

  // ---- 2. STOCKAGE DU CHOIX DE FAÇADE -------------------------------------
  // IMPORTANT : ce site est exécuté dans un « bac à sable » (origine opaque).
  // Dans ce cas, localStorage LÈVE une exception SecurityError dès qu'on y
  // touche. On entoure donc chaque accès d'un try/catch, et on garde la
  // valeur en mémoire (variable) comme solution de repli.
  var memoire = {};
  function lireStockage(cle, defaut) {
    try {
      var v = window.localStorage.getItem(cle);
      return v === null ? defaut : v;
    } catch (e) {
      return memoire.hasOwnProperty(cle) ? memoire[cle] : defaut;
    }
  }
  function ecrireStockage(cle, valeur) {
    memoire[cle] = valeur;                       // repli toujours à jour
    try { window.localStorage.setItem(cle, valeur); } catch (e) { /* bac à sable : on ignore */ }
  }

  // ---- 3. CALCUL DU ZOOM ADAPTATIF ----------------------------------------
  /**
   * Zoom de base : on veut que la scène (largeurScene × hauteurScene unités)
   * tienne en entier dans le canevas, qu'il soit large (ordinateur) ou
   * étroit (téléphone). On prend donc le PLUS PETIT des deux rapports.
   * @param {number} largeur Largeur CSS du canevas (px).
   * @param {number} hauteur Hauteur CSS du canevas (px).
   */
  function calculerZoomBase(largeur, hauteur) {
    return Math.min(largeur / CAM.largeurScene, hauteur / CAM.hauteurScene);
  }

  /** Applique zoom de base × zoom du curseur à l'illustration. */
  function appliquerZoom() {
    illo.zoom = etat.zoomBase * etat.zoomUtilisateur;
  }

  // ---- 4. L'ILLUSTRATION --------------------------------------------------
  var illo = new Zdog.Illustration({
    element: '#maison-canvas',
    dragRotate: true,       // glisser pour tourner librement
    resize: true,           // le canevas épouse son conteneur CSS ; Zdog multiplie
                            // sa résolution par window.devicePixelRatio => rendu net
                            // sur les écrans haute densité (téléphones, Retina…)
    rotate: { x: Maison.VUE_DEPART.x, y: Maison.VUE_DEPART.y },   // vue 3/4 de départ

    onDragStart: function () {
      // Si l'utilisateur glisse, on arrête la caméra automatique / la transition.
      camera.arreter();
      majBoutons();
    },
    onResize: function (largeur, hauteur) {
      // `this` désigne l'illustration. Appelée aussi quand on tourne le téléphone.
      etat.zoomBase = calculerZoomBase(largeur, hauteur);
      this.zoom = etat.zoomBase * etat.zoomUtilisateur;
    },
  });
  etat.zoomBase = calculerZoomBase(illo.width, illo.height);
  appliquerZoom();

  // ---- 5. LE MONDE --------------------------------------------------------
  // Une ancre qui contient toute la scène, descendue de 62 unités pour que le
  // centre de rotation soit à mi-hauteur de la maison (et non au sol).
  var monde = new Zdog.Anchor({
    addTo: illo,
    translate: { y: CAM.decalageVertical },
  });

  // Choix de façade au démarrage (mémorisé si possible).
  var cleFacade = lireStockage('maison-facade', 'vanille');
  if (!Maison.FACADES[cleFacade]) { cleFacade = 'vanille'; }

  Maison.creerSol(monde);

  // Le groupe de la maison : ses enfants sont triés ensemble du plus loin au
  // plus proche (updateSort: true) pour un rendu correct sous tous les angles.
  var groupeMaison = new Zdog.Group({ addTo: monde, updateSort: true });
  var corps = Maison.creerCorps(groupeMaison, Maison.FACADES[cleFacade]);
  Maison.creerVeranda(groupeMaison);
  Maison.creerEscalier(groupeMaison);

  // L'arbre, à gauche de la maison et un peu en avant.
  Maison.creerArbre(monde, { x: -100, z: 22, hauteur: 42, graine: 11, boules: 8 });
  // Un deuxième plus petit à droite, avec une autre graine => une autre forme.
  Maison.creerArbre(monde, { x: 100, z: -62, hauteur: 30, graine: 5, boules: 6 });

  // ---- 6. LA CAMÉRA -------------------------------------------------------
  var texteVue = document.getElementById('info-vue');
  var camera = Maison.creerCamera(illo, function (texte) {
    if (texteVue) { texteVue.textContent = 'Affichage : ' + texte; }
  });

  // ---- 7. INTERFACE -------------------------------------------------------
  var boutonsVue = {
    face: document.getElementById('btn-face'),
    cote: document.getElementById('btn-cote'),
    dessus: document.getElementById('btn-dessus'),
  };
  var boutonAuto = document.getElementById('btn-auto');

  /** Met à jour l'état visuel (aria-pressed) du bouton de rotation automatique. */
  function majBoutons() {
    if (boutonAuto) { boutonAuto.setAttribute('aria-pressed', camera.estAuto() ? 'true' : 'false'); }
  }

  // Un bouton par vue : on parcourt l'objet boutonsVue avec une boucle for…in.
  Object.keys(boutonsVue).forEach(function (cle) {
    var bouton = boutonsVue[cle];
    if (!bouton) { return; }
    bouton.addEventListener('click', function () {
      camera.allerVers(cle);
      majBoutons();
    });
  });

  if (boutonAuto) {
    boutonAuto.addEventListener('click', function () {
      camera.basculerAuto();
      majBoutons();
    });
  }

  // Curseur de zoom.
  var curseurZoom = document.getElementById('ctrl-zoom');
  var texteZoom = document.getElementById('txt-zoom');
  if (curseurZoom) {
    curseurZoom.addEventListener('input', function () {
      etat.zoomUtilisateur = parseFloat(curseurZoom.value);   // la valeur est du texte : on convertit
      appliquerZoom();
      if (texteZoom) { texteZoom.textContent = Math.round(etat.zoomUtilisateur * 100) + ' %'; }
    });
  }

  // Boutons de couleur de façade : GÉNÉRÉS à partir de Maison.FACADES.
  var zoneFacade = document.getElementById('choix-facade');
  if (zoneFacade) {
    Object.keys(Maison.FACADES).forEach(function (cle) {
      var facade = Maison.FACADES[cle];
      var b = document.createElement('button');
      b.type = 'button';
      b.className = 'pastille';
      b.textContent = facade.nom;
      b.style.setProperty('--pastille', facade.mur);        // la couleur sert d'accent CSS
      b.setAttribute('aria-pressed', cle === cleFacade ? 'true' : 'false');
      b.addEventListener('click', function () {
        cleFacade = cle;
        Maison.peindreFacade(corps, facade);                // on repeint, sans reconstruire
        ecrireStockage('maison-facade', cle);
        var tous = zoneFacade.querySelectorAll('button');
        for (var i = 0; i < tous.length; i++) {
          tous[i].setAttribute('aria-pressed', tous[i] === b ? 'true' : 'false');
        }
      });
      zoneFacade.appendChild(b);
    });
  }

  // Information pédagogique : la densité de pixels de l'écran.
  var infoDpr = document.getElementById('info-dpr');
  if (infoDpr) {
    infoDpr.textContent = 'Densité de pixels de votre écran : ×' + (window.devicePixelRatio || 1) +
      ' (le canevas est dessiné en ' + Math.round(illo.width * (window.devicePixelRatio || 1)) + ' × ' +
      Math.round(illo.height * (window.devicePixelRatio || 1)) + ' pixels).';
  }

  // Rotation automatique au démarrage, sauf si l'utilisateur préfère moins d'animations.
  var moinsAnimations = false;
  try {
    moinsAnimations = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
  } catch (e) { /* on garde false */ }
  if (!moinsAnimations) {
    camera.basculerAuto();
    majBoutons();
  }

  // ---- 8. BOUCLE D'ANIMATION ----------------------------------------------
  /**
   * Appelée par le navigateur avant chaque affichage (~60 fois par seconde).
   * @param {number} maintenant Horodatage en millisecondes.
   */
  function animer(maintenant) {
    // deltaTime en secondes, borné pour éviter les sauts après un onglet en pause.
    var dt = Math.min(Math.max((maintenant - etat.dernierTemps) / 1000, 0), 0.1);
    etat.dernierTemps = maintenant;

    camera.mettreAJour(dt);      // fait avancer la transition ou la rotation auto
    illo.updateRenderGraph();    // met à jour puis dessine toute la scène
    requestAnimationFrame(animer);
  }
  requestAnimationFrame(animer);

  // Pour les curieux : on expose la scène et la caméra pour les essayer dans la
  // console du navigateur, par exemple  Maison.illo.rotate.y = 1;
  Maison.illo = illo;
  Maison.camera = camera;
})();