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