263 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			263 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <template>
 | |
| 
 | |
|   <div class="scene" id="scene">
 | |
|     <div class="box" id="box">
 | |
|       <div class="face front">
 | |
|         <img class="cover" src="https://evilspins.com/ES01A/object.png" alt="">
 | |
|       </div>
 | |
|       <div class="face back"></div>
 | |
|       <div class="face right"></div>
 | |
|       <div class="face left"></div>
 | |
|       <div class="face top"></div>
 | |
|       <div class="face bottom"></div>
 | |
|     </div>
 | |
|   </div>
 | |
| </template>
 | |
| 
 | |
| <script setup>
 | |
| /*
 | |
|   Rotation 3D au touch / clic maintenu
 | |
|   - Utilise Pointer Events (fonctionne pour souris, stylet, touch)
 | |
|   - rotateX et rotateY mis à jour pendant le drag
 | |
|   - inertie légère à la release (configurable)
 | |
| */
 | |
| 
 | |
| (function () {
 | |
|   const box = document.getElementById('box');
 | |
|   const scene = document.getElementById('scene');
 | |
| 
 | |
|   // état
 | |
|   let angleX = -20; // angle initial (pour montrer la 3D)
 | |
|   let angleY = 20;
 | |
|   let dragging = false;
 | |
|   let lastPointer = { x: 0, y: 0, time: 0 };
 | |
|   let velocity = { x: 0, y: 0 };
 | |
|   let raf = null;
 | |
| 
 | |
|   // paramètres
 | |
|   const sensitivity = 0.3;    // combien de degrés par px
 | |
|   const friction = 0.95;      // inertie (0..1) ; 1 = pas de friction
 | |
|   const minVelocity = 0.02;   // seuil pour arrêter l'inertie
 | |
|   const enableInertia = true; // tu peux mettre false pour pas d'inertie
 | |
| 
 | |
|   // applique la transformation (raf-friendly)
 | |
|   function applyTransform() {
 | |
|     box.style.transform = `rotateX(${angleX}deg) rotateY(${angleY}deg)`;
 | |
|   }
 | |
| 
 | |
|   applyTransform();
 | |
| 
 | |
|   // boucle d'inertie
 | |
|   function tickInertia() {
 | |
|     if (!enableInertia) return;
 | |
|     // appliquer friction
 | |
|     velocity.x *= friction;
 | |
|     velocity.y *= friction;
 | |
| 
 | |
|     angleX += velocity.y;
 | |
|     angleY += velocity.x;
 | |
| 
 | |
|     // clamp angleX pour éviter flip complet (optionnel)
 | |
|     angleX = Math.max(-80, Math.min(80, angleX));
 | |
| 
 | |
|     applyTransform();
 | |
| 
 | |
|     if (Math.abs(velocity.x) > minVelocity || Math.abs(velocity.y) > minVelocity) {
 | |
|       raf = requestAnimationFrame(tickInertia);
 | |
|     } else {
 | |
|       raf = null;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // pointerdown
 | |
|   scene.addEventListener('pointerdown', (ev) => {
 | |
|     ev.preventDefault();
 | |
|     dragging = true;
 | |
|     scene.setPointerCapture(ev.pointerId);
 | |
|     lastPointer.x = ev.clientX;
 | |
|     lastPointer.y = ev.clientY;
 | |
|     lastPointer.time = performance.now();
 | |
|     velocity.x = 0;
 | |
|     velocity.y = 0;
 | |
|     if (raf) { cancelAnimationFrame(raf); raf = null; } // stop inertie en commençant un nouveau drag
 | |
|   });
 | |
| 
 | |
|   // pointermove
 | |
|   scene.addEventListener('pointermove', (ev) => {
 | |
|     if (!dragging) return;
 | |
|     ev.preventDefault();
 | |
|     const now = performance.now();
 | |
|     const dx = ev.clientX - lastPointer.x;
 | |
|     const dy = ev.clientY - lastPointer.y;
 | |
| 
 | |
|     // mise à jour angles (inverser si tu veux sens différent)
 | |
|     angleY += dx * sensitivity;
 | |
|     angleX -= dy * sensitivity;
 | |
| 
 | |
|     // clamp angleX
 | |
|     angleX = Math.max(-80, Math.min(80, angleX));
 | |
| 
 | |
|     // calculer vitesse (px per frame approximative)
 | |
|     const dt = Math.max(1, now - lastPointer.time); // ms
 | |
|     velocity.x = (dx / dt) * 16 * sensitivity; // normalisé approximativement à 60fps
 | |
|     velocity.y = (-dy / dt) * 16 * sensitivity;
 | |
| 
 | |
|     lastPointer.x = ev.clientX;
 | |
|     lastPointer.y = ev.clientY;
 | |
|     lastPointer.time = now;
 | |
| 
 | |
|     applyTransform();
 | |
|   });
 | |
| 
 | |
|   // pointerup / cancel
 | |
|   const endDrag = (ev) => {
 | |
|     if (!dragging) return;
 | |
|     dragging = false;
 | |
|     try { scene.releasePointerCapture(ev.pointerId); } catch (e) { }
 | |
|     // si inertie activée, lancer la boucle
 | |
|     if (enableInertia && (Math.abs(velocity.x) > minVelocity || Math.abs(velocity.y) > minVelocity)) {
 | |
|       if (!raf) raf = requestAnimationFrame(tickInertia);
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   scene.addEventListener('pointerup', endDrag);
 | |
|   scene.addEventListener('pointercancel', endDrag);
 | |
|   scene.addEventListener('pointerleave', endDrag); // utile sur desktop si tu sors du container
 | |
| 
 | |
|   // accessibility : permettre rotation via clavier (optionnel)
 | |
|   scene.tabIndex = 0;
 | |
|   scene.addEventListener('keydown', (e) => {
 | |
|     const step = 8;
 | |
|     if (e.key === 'ArrowLeft') { angleY -= step; applyTransform(); }
 | |
|     if (e.key === 'ArrowRight') { angleY += step; applyTransform(); }
 | |
|     if (e.key === 'ArrowUp') { angleX -= step; applyTransform(); }
 | |
|     if (e.key === 'ArrowDown') { angleX += step; applyTransform(); }
 | |
|   });
 | |
| 
 | |
| })();
 | |
| </script>
 | |
| 
 | |
| <style>
 | |
| /*  H      W   D
 | |
|     k7  5   :  8 : 1
 | |
|     CD  12  : 14 : 1
 | |
|     VHS 4.5 :  8 : 1
 | |
|     DVD 14  : 10 : 1
 | |
|     */
 | |
| 
 | |
| :root {
 | |
|   --size: 100px;
 | |
|   --height: 150px;
 | |
|   --width: 150px;
 | |
|   --depth: 10px;
 | |
|   --bg: #0f172a;
 | |
| }
 | |
| 
 | |
| html,
 | |
| body {
 | |
|   height: 100%;
 | |
|   margin: 0;
 | |
|   background: var(--bg);
 | |
|   display: flex;
 | |
|   align-items: center;
 | |
|   justify-content: center;
 | |
|   font-family: system-ui, Segoe UI, Roboto, Helvetica, Arial;
 | |
|   -webkit-font-smoothing: antialiased;
 | |
| }
 | |
| 
 | |
| /* scène avec perspective */
 | |
| .scene {
 | |
|   width: calc(var(--height) * 1.2);
 | |
|   height: calc(var(--width) * 1.2);
 | |
|   perspective: 1000px;
 | |
|   touch-action: none;
 | |
|   /* essentiel pour empêcher le scroll pendant le drag */
 | |
| }
 | |
| 
 | |
| /* l'objet 3D (box simple) */
 | |
| .box {
 | |
|   width: var(--width);
 | |
|   height: var(--height);
 | |
|   position: relative;
 | |
|   transform-style: preserve-3d;
 | |
|   transition: transform 120ms linear;
 | |
|   /* légère smoothing quand on lâche */
 | |
|   margin: auto;
 | |
|   user-select: none;
 | |
|   cursor: grab;
 | |
| }
 | |
| 
 | |
| .box:active {
 | |
|   cursor: grabbing;
 | |
| }
 | |
| 
 | |
| /* faces du box */
 | |
| .face {
 | |
|   position: absolute;
 | |
|   display: flex;
 | |
|   align-items: center;
 | |
|   justify-content: center;
 | |
|   font-size: 20px;
 | |
|   color: white;
 | |
|   font-weight: 600;
 | |
|   backface-visibility: hidden;
 | |
|   border: 2px solid rgba(255, 255, 255, 0.06);
 | |
|   box-sizing: border-box;
 | |
|   transform-origin: top right;
 | |
| }
 | |
| 
 | |
| .front,
 | |
| .back {
 | |
|   width: 100%;
 | |
|   height: 100%;
 | |
| }
 | |
| 
 | |
| .face.top,
 | |
| .face.bottom {
 | |
|   width: var(--width);
 | |
|   height: var(--depth);
 | |
| }
 | |
| 
 | |
| .face.left,
 | |
| .face.right {
 | |
|   width: var(--depth);
 | |
|   height: var(--height);
 | |
| }
 | |
| 
 | |
| .face.front {
 | |
|   background: linear-gradient(180deg, #2563eb, #7dd3fc);
 | |
|   transform: translateX(0px) translateY(0px) translateZ(0px);
 | |
| }
 | |
| 
 | |
| .face.back {
 | |
|   background: linear-gradient(180deg, #7c3aed, #a78bfa);
 | |
|   transform: rotateY(180deg) translateX(var(--width)) translateY(0px) translateZ(var(--depth));
 | |
| }
 | |
| 
 | |
| .face.right {
 | |
|   background: linear-gradient(180deg, #10b981, #6ee7b7);
 | |
|   transform: rotateY(90deg) translateX(0px) translateY(0px) translateZ(var(--width));
 | |
|   transform-origin: top left;
 | |
| }
 | |
| 
 | |
| .face.left {
 | |
|   background: linear-gradient(180deg, #ef4444, #fca5a5);
 | |
|   transform: rotateY(-90deg) translateX(0) translateY(0px) translateZ(var(--depth));
 | |
| }
 | |
| 
 | |
| .face.top {
 | |
|   background: linear-gradient(180deg, #f59e0b, #fcd34d);
 | |
|   transform: rotateX(90deg) translateX(0px) translateY(calc(var(--depth) * -1)) translateZ(0px);
 | |
| }
 | |
| 
 | |
| .face.bottom {
 | |
|   background: linear-gradient(180deg, #06b6d4, #67e8f9);
 | |
|   transform: rotateX(-90deg) translateX(0) translateY(0px) translateZ(calc(var(--height)));
 | |
| }
 | |
| 
 | |
| .cover {
 | |
|   height: 100%;
 | |
|   width: 100%;
 | |
|   object-fit: cover;
 | |
| }
 | |
| </style> |