Files
evilspins/app/components/box.vue
valere 9e697822e4
All checks were successful
Deploy App / deploy (push) Successful in 1m49s
studio v1
2025-09-20 17:18:29 +02:00

327 lines
8.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- scène 3D -->
<div ref="scene" class="scene z-10">
<div ref="box" class="box">
<div class="face front relative" ref="frontFace">
<img class="cover absolute" :src="`/${compilation.id}/cover.jpg`" alt="">
</div>
<div class="face back" ref="backFace" />
<div class="face right" ref="rightFace" />
<div class="face left" ref="leftFace" />
<div class="face top" ref="topFace">
<img class="logo h-full p-1" src="/logo.svg" alt="">
<img class="absolute block h-1/2" style="left:5%;" :src="`/${compilation.id}/title.svg`" alt="">
</div>
<div class="face bottom" ref="bottomFace" />
</div>
</div>
</template>
<script setup lang="ts">
import type { Compilation, BoxPosition } from '~~/types/types';
const props = withDefaults(
defineProps<{
compilation: Compilation
position?: BoxPosition
size?: number
}>(),
{
position: () => ({ x: 0, y: 0, z: 0 }),
size: 6,
}
)
// States
const angleX = ref(props.position.x)
const angleY = ref(props.position.y)
const angleZ = ref(props.position.z)
const frontFace = ref()
const backFace = ref()
const rightFace = ref()
const leftFace = ref()
const topFace = ref()
const bottomFace = ref()
const box = ref()
const scene = ref()
/*
ÉTATS POUR LE DRAG + INERTIE
*/
let dragging = false
let lastPointer = { x: 0, y: 0, time: 0 } // position précédente du pointeur
let velocity = { x: 0, y: 0 } // vitesse calculée pour inertie
let raf = null // id du requestAnimationFrame
/*
PARAMÈTRES DE RÉGLAGE
*/
const sensitivity = 0.3 // combien de degrés par pixel de mouvement
const friction = 0.95 // inertie : 1 = sans perte, plus bas = ralentit vite
const minVelocity = 0.02 // seuil sous lequel on arrête linertie
const enableInertia = true // true = inertie activée, false = rotation immédiate sans suite
/*
Applique la transformation CSS à la box
*/
function applyTransform() {
angleX.value = Math.round(angleX.value)
angleY.value = Math.round(angleY.value)
angleZ.value = Math.round(angleZ.value)
box.value.style.transform = `rotateX(${angleX.value}deg) rotateY(${angleY.value}deg) rotateZ(${angleZ.value}deg)`
}
function applySize() {
updateCssVar('--height', `${props.size * (100 / 3)}px`, scene.value)
updateCssVar('--width', `${props.size * 50}px`, scene.value)
updateCssVar('--depth', `${props.size * 10}px`, scene.value)
}
function applyColor() {
frontFace.value.style.setProperty('background', `${props.compilation.color2}`)
backFace.value.style.setProperty('background', `linear-gradient(to top, ${props.compilation.color1}, ${props.compilation.color2})`)
rightFace.value.style.setProperty('background', `linear-gradient(to top, ${props.compilation.color1}, ${props.compilation.color2})`)
leftFace.value.style.setProperty('background', `linear-gradient(to top, ${props.compilation.color1}, ${props.compilation.color2})`)
topFace.value.style.setProperty('background', `linear-gradient(to top, ${props.compilation.color2}, ${props.compilation.color2})`)
bottomFace.value.style.setProperty('background', `${props.compilation.color1}`)
}
/*
Fonction utilitaire : place la box directement à une rotation donnée
*/
function applyRotation() {
angleX.value = props.position.x
angleY.value = props.position.y
angleZ.value = props.position.z
applyTransform()
box.value.style.setProperty('transition', 'transform 800ms ease-in-out')
setTimeout(() => {
box.value.style.setProperty('transition', 'transform 120ms linear')
}, 120)
}
/*
Boucle dinertie après un drag
- reprend la vitesse calculée à la release
- diminue petit à petit avec friction
- stoppe quand vitesse < minVelocity
*/
function tickInertia() {
if (!enableInertia) return
// appliquer la friction
velocity.x *= friction
velocity.y *= friction
// appliquer au box
angleX.value += velocity.y
angleY.value += velocity.x
// clamp angleX pour éviter de retourner la box trop loin
angleX.value = Math.max(-80, Math.min(80, angleX.value))
applyTransform()
// continuer tant quil reste du mouvement
if (Math.abs(velocity.x) > minVelocity || Math.abs(velocity.y) > minVelocity) {
raf = requestAnimationFrame(tickInertia)
} else {
raf = null
}
}
/*
Mise en place des listeners une fois le composant monté
*/
onMounted(() => {
applySize()
applyColor()
applyTransform()
// pointerdown = début du drag
const down = (ev) => {
ev.preventDefault()
dragging = true
scene.value.setPointerCapture(ev.pointerId)
lastPointer = { x: ev.clientX, y: ev.clientY, time: performance.now() }
velocity = { x: 0, y: 0 }
if (raf) { cancelAnimationFrame(raf); raf = null }
}
// pointermove = on bouge la souris ou le doigt
const move = (ev) => {
if (!dragging) return
ev.preventDefault()
const now = performance.now()
const dx = ev.clientX - lastPointer.x
const dy = ev.clientY - lastPointer.y
// mise à jour des angles
angleY.value += dx * sensitivity
angleX.value -= dy * sensitivity
angleX.value = Math.max(-80, Math.min(80, angleX.value))
// calcul vitesse pour inertie
const dt = Math.max(1, now - lastPointer.time)
velocity.x = (dx / dt) * 16 * sensitivity
velocity.y = (-dy / dt) * 16 * sensitivity
lastPointer = { x: ev.clientX, y: ev.clientY, time: now }
applyTransform()
}
// pointerup = fin du drag
const end = (ev) => {
if (!dragging) return
dragging = false
try { scene.value.releasePointerCapture(ev.pointerId) } catch { }
if (enableInertia && (Math.abs(velocity.x) > minVelocity || Math.abs(velocity.y) > minVelocity)) {
if (!raf) raf = requestAnimationFrame(tickInertia)
}
}
// attach des events
scene.value.addEventListener('pointerdown', down)
scene.value.addEventListener('pointermove', move)
scene.value.addEventListener('pointerup', end)
scene.value.addEventListener('pointercancel', end)
scene.value.addEventListener('pointerleave', end)
// cleanup au démontage
onBeforeUnmount(() => {
cancelAnimationFrame(raf)
})
})
watch(() => props.position, () => {
applyRotation()
}, { deep: true })
watch(() => props.compilation, () => {
applyColor()
}, { deep: true })
watch(() => props.size, () => {
applySize()
})
</script>
<style>
html,
body {
height: 100%;
margin: 0;
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 {
height: var(--height);
width: var(--width);
perspective: 1000px;
touch-action: none;
/* essentiel pour empêcher le scroll pendant le drag */
/* height: 20px; */
transition: all .5s;
}
/* l'objet 3D (box simple) */
.box {
width: var(--width);
height: var(--height);
position: relative;
transform-style: preserve-3d;
transition: transform 120ms linear;
transition: height 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 {
transform: translateX(0px) translateY(0px) translateZ(0px);
}
.face.back {
transform: rotateY(180deg) translateX(var(--width)) translateY(0px) translateZ(var(--depth));
}
.face.right {
transform: rotateY(90deg) translateX(0px) translateY(0px) translateZ(var(--width));
transform-origin: top left;
}
.face.left {
transform: rotateY(-90deg) translateX(0) translateY(0px) translateZ(var(--depth));
}
.face.top {
transform: rotateX(90deg) translateX(0px) translateY(calc(var(--depth) * -1)) translateZ(0px);
}
.face.top>* {
@apply rotate-180;
}
.face.bottom {
transform: rotateX(-90deg) translateX(0) translateY(0px) translateZ(calc(var(--height)));
}
.cover {
height: 100%;
width: 100%;
object-fit: cover;
}
.logo {
filter: drop-shadow(2px 2px 0 rgb(0 0 0 / 0.8));
}
</style>