This commit is contained in:
263
app/components/boxItem.vue
Normal file
263
app/components/boxItem.vue
Normal file
@@ -0,0 +1,263 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user