Vue 360° Produit Bootstrap 5 Interactive

Extraits & Composants HTML 08/04/2026 17:00:00 angularforall.com
Bootstrap 5 Vue 360 Produit Interactive Rotation E Commerce Template Html Css Js

Visionneuse produit 360° Bootstrap 5 : rotation interactive à la souris ou au touch, navigation par flèches, indicateur de progression et animation fluide.

<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8" />
  <meta name="copyright" content="AngularForAll" />
  <meta name="author" content="AngularForAll" />
  <meta name="robots" content="noindex, nofollow" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta http-equiv="Cache-Control" content="public, max-age=604800" />
  <title>Snippets View360 Product Bootstrap5 2026 05020020 | AngularForAll</title>
<!-- Bootstrap 5 CSS -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
  <!-- Font Awesome pour les icônes -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
  <style>
    body {
      background-color: #f8f9fa;
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      user-select: none;
    }
    .viewer-container {
      max-width: 700px;
      margin: 0 auto;
      position: relative;
      background: #ffffff;
      border-radius: 24px;
      box-shadow: 0 25px 50px -12px rgba(0,0,0,0.15);
      padding: 1.5rem;
      margin-top: 2rem;
      margin-bottom: 2rem;
    }
    .viewer-title {
      font-weight: 700;
      letter-spacing: -0.5px;
    }
    .viewer-badge {
      font-size: 0.8rem;
      background: #e9ecef;
      color: #495057;
      padding: 0.35em 0.9em;
      border-radius: 20px;
      font-weight: 500;
    }
    .image-wrapper {
      position: relative;
      width: 100%;
      max-width: 500px;
      margin: 0 auto;
      cursor: grab;
      background: #f1f3f5;
      border-radius: 16px;
      overflow: hidden;
      box-shadow: inset 0 2px 8px rgba(0,0,0,0.03);
      aspect-ratio: 1 / 1;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    .image-wrapper:active {
      cursor: grabbing;
    }
    .image-wrapper img {
      width: 100%;
      height: 100%;
      object-fit: contain;
      pointer-events: none;
      transition: opacity 0.08s ease;
    }
    .rotation-controls {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 1.2rem;
      margin-top: 1.8rem;
    }
    .btn-rotate {
      width: 52px;
      height: 52px;
      border-radius: 50%;
      background: white;
      border: 1px solid #dee2e6;
      color: #212529;
      font-size: 1.4rem;
      display: flex;
      align-items: center;
      justify-content: center;
      transition: all 0.2s ease;
      box-shadow: 0 4px 10px rgba(0,0,0,0.04);
      cursor: pointer;
    }
    .btn-rotate:hover {
      background: #f1f3f5;
      border-color: #adb5bd;
      box-shadow: 0 8px 16px rgba(0,0,0,0.08);
    }
    .btn-rotate:active {
      background: #e9ecef;
      transform: scale(0.96);
    }
    .frame-indicator {
      background: #ffffff;
      border: 1px solid #dee2e6;
      border-radius: 40px;
      padding: 0.4rem 1.4rem;
      font-weight: 600;
      font-size: 0.9rem;
      color: #495057;
      min-width: 120px;
      text-align: center;
      background: #f8f9fa;
      box-shadow: 0 2px 6px rgba(0,0,0,0.02);
    }
    .drag-hint {
      font-size: 0.8rem;
      color: #868e96;
      margin-top: 0.6rem;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 0.3rem;
    }
    .auto-rotate-btn {
      margin-top: 0.8rem;
      font-size: 0.85rem;
      border-radius: 30px;
      padding: 0.4rem 1.2rem;
      transition: 0.2s;
    }
    .footer-note {
      font-size: 0.85rem;
      color: #adb5bd;
    }
    @media (max-width: 576px) {
      .viewer-container {
        padding: 1.2rem;
      }
    }
  </style>
</head>
<body class="d-flex flex-column min-vh-100">

  <!-- Navigation simple -->
  <nav class="navbar navbar-expand-lg bg-white border-bottom shadow-sm">
    <div class="container">
      <a class="navbar-brand fw-bold" href="#">
        <i class="fa-solid fa-cube me-2 text-primary"></i>ThreeSixtyView
      </a>
      <div class="d-flex">
        <span class="viewer-badge"><i class="fa-regular fa-eye me-1"></i> Rotation 360°</span>
      </div>
    </div>
  </nav>

  <!-- Contenu principal -->
  <main class="flex-grow-1 d-flex align-items-center justify-content-center p-3">
    <div class="viewer-container text-center">
      
      <div class="mb-3">
        <h2 class="viewer-title mb-1">Chaussure Sportswear</h2>
        <p class="text-secondary mb-0"><i class="fa-solid fa-rotate me-1"></i> Faites glisser pour explorer</p>
      </div>
      
      <!-- Zone de visualisation 360 -->
      <div class="image-wrapper" id="viewerWrapper">
        <img id="productImage" src="https://placehold.co/600x600/e9ecef/495057?text=Frame+1" alt="Vue 360° du produit">
      </div>
      
      <!-- Contrôles de rotation -->
      <div class="rotation-controls">
        <button class="btn-rotate" id="rotateLeft" title="Rotation gauche">
          <i class="fa-solid fa-chevron-left"></i>
        </button>
        <div class="frame-indicator" id="frameDisplay">
          <span id="currentFrame">1</span> / <span id="totalFrames">36</span>
        </div>
        <button class="btn-rotate" id="rotateRight" title="Rotation droite">
          <i class="fa-solid fa-chevron-right"></i>
        </button>
      </div>
      
      <!-- Bouton auto-rotation et hint -->
      <div class="d-flex justify-content-center align-items-center gap-3 mt-3 flex-wrap">
        <button class="btn btn-outline-secondary auto-rotate-btn" id="autoRotateToggle">
          <i class="fa-solid fa-play me-1"></i> Rotation automatique
        </button>
      </div>
      <div class="drag-hint mt-2">
        <i class="fa-solid fa-hand-pointer"></i> Cliquez-glissez sur l'image pour pivoter
      </div>
      
      <!-- Note pédagogique : remplacer les images -->
      <div class="mt-3 p-2 bg-light rounded-3 small text-muted">
        <i class="fa-solid fa-circle-info me-1"></i> 
        Remplacez les images dans le dossier <code>/images/360/</code> (frame_01.jpg à frame_36.jpg)
      </div>
    </div>
  </main>

  <footer class="bg-white border-top py-3 mt-auto">
    <div class="container text-center footer-note">
      <span>© 2026 ThreeSixtyView — Expérience produit interactive</span>
    </div>
  </footer>

  <!-- Bootstrap JS -->
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
  
  <!-- Script 360° interactif -->
  <script>
    (function() {
      // ---------- CONFIGURATION ----------
      // Nombre total d'images dans la séquence 360° (modifiable)
      const TOTAL_FRAMES = 36;
      
      // Format du nom de fichier : frame_01.jpg, frame_02.jpg ... frame_36.jpg
      // Adaptez le chemin selon votre structure. Exemple : "images/360/frame_"
      const IMAGE_PATH = "https://placehold.co/600x600/e9ecef/495057?text=Frame+";
      // Pour un vrai projet, utilisez quelque chose comme : "images/produit/frame_" et extension .jpg
      // Nous utilisons placehold.co pour la démo, les images montreront "Frame X"
      
      // Vitesse de l'auto-rotation (ms entre chaque frame)
      const AUTO_ROTATE_SPEED = 80;
      
      // ---------- ÉTAT ----------
      let currentFrame = 1;            // Frame actuelle (1-indexé)
      let isDragging = false;
      let startX = 0;
      let lastFrameOnDrag = 1;
      let autoRotateInterval = null;
      let autoRotateActive = false;
      
      // Éléments DOM
      const imgElement = document.getElementById('productImage');
      const wrapper = document.getElementById('viewerWrapper');
      const currentFrameSpan = document.getElementById('currentFrame');
      const totalFramesSpan = document.getElementById('totalFrames');
      const rotateLeftBtn = document.getElementById('rotateLeft');
      const rotateRightBtn = document.getElementById('rotateRight');
      const autoRotateBtn = document.getElementById('autoRotateToggle');
      
      // Mise à jour de l'affichage du total
      totalFramesSpan.textContent = TOTAL_FRAMES;
      
      // ---------- FONCTIONS ----------
      
      // Génère l'URL de l'image pour un numéro de frame donné
      function getImageUrl(frameNumber) {
        // Formatage avec zéro devant si besoin (01, 02...)
        const paddedNumber = String(frameNumber).padStart(2, '0');
        // Pour la démo avec placehold.co, on utilise le numéro directement
        // Dans un vrai projet : return `${IMAGE_PATH}${paddedNumber}.jpg`;
        return `${IMAGE_PATH}${frameNumber}`;
      }
      
      // Met à jour l'image affichée et l'indicateur
      function updateFrame(frameNumber) {
        // S'assurer que la frame reste dans les limites [1, TOTAL_FRAMES]
        if (frameNumber < 1) {
          frameNumber = TOTAL_FRAMES;
        } else if (frameNumber > TOTAL_FRAMES) {
          frameNumber = 1;
        }
        
        currentFrame = frameNumber;
        
        // Change l'image (avec un léger effet visuel)
        imgElement.style.opacity = '0.85';
        imgElement.src = getImageUrl(currentFrame);
        
        // Réafficher l'image une fois chargée (ou presque)
        imgElement.onload = () => {
          imgElement.style.opacity = '1';
        };
        // Si l'image est en cache, onload peut ne pas se déclencher, on force après un court délai
        setTimeout(() => {
          imgElement.style.opacity = '1';
        }, 30);
        
        // Mise à jour du texte
        currentFrameSpan.textContent = currentFrame;
      }
      
      // Rotation vers la gauche (frame précédente)
      function rotateLeft() {
        updateFrame(currentFrame - 1);
      }
      
      // Rotation vers la droite (frame suivante)
      function rotateRight() {
        updateFrame(currentFrame + 1);
      }
      
      // Active/désactive l'auto-rotation
      function toggleAutoRotate() {
        if (autoRotateActive) {
          stopAutoRotate();
        } else {
          startAutoRotate();
        }
      }
      
      function startAutoRotate() {
        if (autoRotateInterval) return;
        autoRotateActive = true;
        autoRotateBtn.innerHTML = '<i class="fa-solid fa-pause me-1"></i> Pause';
        autoRotateBtn.classList.remove('btn-outline-secondary');
        autoRotateBtn.classList.add('btn-dark');
        
        autoRotateInterval = setInterval(() => {
          rotateRight(); // tourne dans le sens horaire par défaut
        }, AUTO_ROTATE_SPEED);
      }
      
      function stopAutoRotate() {
        if (autoRotateInterval) {
          clearInterval(autoRotateInterval);
          autoRotateInterval = null;
        }
        autoRotateActive = false;
        autoRotateBtn.innerHTML = '<i class="fa-solid fa-play me-1"></i> Rotation automatique';
        autoRotateBtn.classList.remove('btn-dark');
        autoRotateBtn.classList.add('btn-outline-secondary');
      }
      
      // Gestion du drag (mouse et touch)
      function onDragStart(e) {
        // Empêche le drag si on clique sur un bouton ou autre
        if (e.target.closest('button')) return;
        
        e.preventDefault();
        isDragging = true;
        lastFrameOnDrag = currentFrame;
        
        // Récupère la position X de départ (souris ou tactile)
        if (e.type === 'mousedown') {
          startX = e.clientX;
        } else if (e.type === 'touchstart') {
          startX = e.touches[0].clientX;
        }
        
        // Arrêter l'auto-rotation si on commence à faire glisser
        if (autoRotateActive) {
          stopAutoRotate();
        }
        
        wrapper.style.cursor = 'grabbing';
      }
      
      function onDragMove(e) {
        if (!isDragging) return;
        e.preventDefault();
        
        let currentX;
        if (e.type === 'mousemove') {
          currentX = e.clientX;
        } else if (e.type === 'touchmove') {
          currentX = e.touches[0].clientX;
        } else {
          return;
        }
        
        // Calcul du déplacement horizontal
        const deltaX = currentX - startX;
        
        // Sensibilité : un déplacement de 15px change de frame (ajustable)
        const pixelsPerFrame = 15;
        
        // Calcul du décalage en nombre de frames
        const frameShift = Math.round(deltaX / pixelsPerFrame);
        
        // Nouvelle frame basée sur celle de départ du drag
        let newFrame = lastFrameOnDrag + frameShift;
        
        // Gestion du bouclage (modulo)
        // On normalise entre 1 et TOTAL_FRAMES
        newFrame = ((newFrame - 1) % TOTAL_FRAMES + TOTAL_FRAMES) % TOTAL_FRAMES + 1;
        
        // Ne mettre à jour que si la frame a changé
        if (newFrame !== currentFrame) {
          updateFrame(newFrame);
        }
      }
      
      function onDragEnd(e) {
        if (!isDragging) return;
        isDragging = false;
        wrapper.style.cursor = 'grab';
        
        // Réinitialiser les variables de drag
        startX = 0;
        lastFrameOnDrag = currentFrame;
      }
      
      // Empêcher le comportement par défaut sur les mobiles pour éviter le scroll
      function preventTouchDefaults(e) {
        if (isDragging || e.target.closest('#viewerWrapper')) {
          e.preventDefault();
        }
      }
      
      // ---------- ÉVÉNEMENTS ----------
      
      // Souris
      wrapper.addEventListener('mousedown', onDragStart);
      window.addEventListener('mousemove', onDragMove);
      window.addEventListener('mouseup', onDragEnd);
      
      // Tactile
      wrapper.addEventListener('touchstart', onDragStart, { passive: false });
      window.addEventListener('touchmove', onDragMove, { passive: false });
      window.addEventListener('touchend', onDragEnd);
      window.addEventListener('touchcancel', onDragEnd);
      
      // Empêche le défilement sur mobile lorsqu'on interagit avec le viewer
      document.addEventListener('touchmove', preventTouchDefaults, { passive: false });
      
      // Boutons de rotation
      rotateLeftBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        if (autoRotateActive) stopAutoRotate();
        rotateLeft();
      });
      
      rotateRightBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        if (autoRotateActive) stopAutoRotate();
        rotateRight();
      });
      
      // Bouton auto-rotation
      autoRotateBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        toggleAutoRotate();
      });
      
      // Style curseur par défaut
      wrapper.style.cursor = 'grab';
      
      // Initialisation : charger la première image
      updateFrame(1);
      
      // Nettoyage optionnel (bonne pratique si la page était dynamique)
      window.addEventListener('beforeunload', () => {
        stopAutoRotate();
      });
      
      console.log('✅ ThreeSixtyView initialisé avec ' + TOTAL_FRAMES + ' frames.');
    })();
  </script>
</body>
</html>

Télécharger le fichier source

Partager