Files
evilspins/app/components/molecule/box.vue
valere 96ffb4b10a
All checks were successful
Deploy App / build (push) Successful in 1m20s
Deploy App / deploy (push) Successful in 16s
add working player
2025-10-04 00:49:12 +02:00

318 lines
8.2 KiB
Vue

<template>
<article class="box box-scene z-10" ref="scene">
<div class="box-object" ref="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">
{{ compilation.description }}
</div>
<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>
<OrganismCompilationPage :compilation="compilation" class="box-page" v-if="props.BoxState === 'selected'" />
</article>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import type { Compilation, BoxState } from '~~/types/types'
const props = withDefaults(
defineProps<{
compilation: Compilation
BoxState?: BoxState
}>(),
{ BoxState: 'list' }
)
const isDraggable = computed(() => !['list', 'hidden'].includes(BoxState.value()))
// --- Réfs ---
const scene = ref<HTMLElement>()
const box = ref<HTMLElement>()
const frontFace = ref<HTMLElement>()
const backFace = ref<HTMLElement>()
const rightFace = ref<HTMLElement>()
const leftFace = ref<HTMLElement>()
const topFace = ref<HTMLElement>()
const bottomFace = ref<HTMLElement>()
// --- Angles ---
const rotateX = ref(0)
const rotateY = ref(0)
const rotateZ = ref(0)
// --- Drag + inertie ---
let dragging = false
let lastPointer = { x: 0, y: 0, time: 0 }
let velocity = { x: 0, y: 0 }
let raf: number | null = null
const sensitivity = 0.3
const friction = 0.95
const minVelocity = 0.02
const enableInertia = true
// --- Transformations ---
function applyTransform(duration = 0.5) {
if (!box.value) return
rotateX.value = Math.round(rotateX.value)
rotateY.value = Math.round(rotateY.value)
rotateZ.value = Math.round(rotateZ.value)
box.value.style.transition = `transform ${duration}s ease`
box.value.style.transform = `rotateX(${rotateX.value}deg) rotateY(${rotateY.value}deg) rotateZ(${rotateZ.value}deg)`
}
// --- Gestion BoxState ---
function applyBoxState() {
switch (props.BoxState) {
case 'list':
rotateX.value = 76
rotateY.value = 0
rotateZ.value = 150
break
case 'selected':
rotateX.value = -20
rotateY.value = 20
rotateZ.value = 0
break
case 'hide':
rotateX.value = 76
rotateY.value = 0
rotateZ.value = 150
break
}
applyTransform(0.8) // transition fluide
}
// --- Couleurs ---
function applyColor() {
if (!frontFace.value || !backFace.value || !leftFace.value || !topFace.value || !bottomFace.value) return
frontFace.value.style.background = props.compilation.color2
backFace.value.style.background = `linear-gradient(to top, ${props.compilation.color1}, ${props.compilation.color2})`
leftFace.value.style.background = `linear-gradient(to top, ${props.compilation.color1}, ${props.compilation.color2})`
rightFace.value.style.background = `linear-gradient(to top, ${props.compilation.color1}, ${props.compilation.color2})`
topFace.value.style.background = `linear-gradient(to top, ${props.compilation.color2}, ${props.compilation.color2})`
bottomFace.value.style.background = props.compilation.color1
}
// --- Inertie ---
function tickInertia() {
if (!enableInertia) return
velocity.x *= friction
velocity.y *= friction
rotateX.value += velocity.y
rotateY.value += velocity.x
rotateX.value = Math.max(-80, Math.min(80, rotateX.value))
applyTransform(0.05) // court duration pour inertie fluide
if (Math.abs(velocity.x) > minVelocity || Math.abs(velocity.y) > minVelocity) {
raf = requestAnimationFrame(tickInertia)
} else {
raf = null
}
}
// --- Pointer events ---
let listenersAttached = false
const down = (ev: PointerEvent) => {
ev.preventDefault()
dragging = true
box.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 }
}
const move = (ev: PointerEvent) => {
if (!dragging) return
ev.preventDefault()
const now = performance.now()
const dx = ev.clientX - lastPointer.x
const dy = ev.clientY - lastPointer.y
const dt = Math.max(1, now - lastPointer.time)
rotateY.value += dx * sensitivity
rotateX.value -= dy * sensitivity
rotateX.value = Math.max(-80, Math.min(80, rotateX.value))
velocity.x = (dx / dt) * 16 * sensitivity
velocity.y = (-dy / dt) * 16 * sensitivity
lastPointer = { x: ev.clientX, y: ev.clientY, time: now }
applyTransform(0) // immédiat pendant drag
}
const end = (ev: PointerEvent) => {
if (!dragging) return
dragging = false
try { box.value?.releasePointerCapture(ev.pointerId) } catch { }
if (enableInertia && (Math.abs(velocity.x) > minVelocity || Math.abs(velocity.y) > minVelocity)) {
if (!raf) raf = requestAnimationFrame(tickInertia)
}
}
function addListeners() {
if (!box.value || listenersAttached) return
box.value.addEventListener('pointerdown', down)
box.value.addEventListener('pointermove', move)
box.value.addEventListener('pointerup', end)
box.value.addEventListener('pointercancel', end)
box.value.addEventListener('pointerleave', end)
listenersAttached = true
}
function removeListeners() {
if (!box.value || !listenersAttached) return
box.value.removeEventListener('pointerdown', down)
box.value.removeEventListener('pointermove', move)
box.value.removeEventListener('pointerup', end)
box.value.removeEventListener('pointercancel', end)
box.value.removeEventListener('pointerleave', end)
listenersAttached = false
}
onMounted(() => {
applyColor()
applyBoxState()
if (isDraggable) addListeners()
})
onBeforeUnmount(() => {
cancelAnimationFrame(raf!)
removeListeners()
})
// --- Watchers ---
watch(() => props.BoxState, () => applyBoxState())
watch(() => props.compilation, () => applyColor(), { deep: true })
watch(() => isDraggable, (enabled) => enabled ? addListeners() : removeListeners())
</script>
<style lang="scss" scoped>
.box {
--size: 6px;
--height: calc(var(--size) * (100 / 3));
--width: calc(var(--size) * 50);
--depth: calc(var(--size) * 10);
transition: all .5s;
&.hide {
height: 0;
opacity: 0;
z-index: 0;
}
&.list {
@apply hover:scale-105;
}
&-scene {
height: calc(var(--size) * 20);
perspective: 1000px;
touch-action: none;
}
&-object {
width: var(--width);
height: var(--height);
position: relative;
transform-style: preserve-3d;
margin: auto;
user-select: none;
.list & {
cursor: pointer;
}
.selected & {
cursor: grab;
}
&:active {
cursor: grabbing;
}
}
.face {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: white;
font-weight: 600;
backface-visibility: hidden;
box-sizing: border-box;
border: 1px solid black;
}
.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(0) translateY(0) translateZ(var(--depth));
}
.face.back {
transform: rotateY(180deg) translateX(0) translateY(0) translateZ(0);
}
.face.right {
transform: rotateY(90deg) translateX(calc(var(--depth)*-1)) translateY(0px) translateZ(var(--width));
transform-origin: top left;
}
.face.left {
transform: rotateY(-90deg) translateX(calc(var(--depth)/2)) translateY(0) translateZ(calc(var(--depth)/2));
}
.face.top {
transform: rotateX(90deg) translateX(0px) translateY(calc(var(--depth)/2)) translateZ(calc(var(--depth)/2));
}
.face.top>* {
@apply rotate-180;
}
.face.bottom {
transform: rotateX(-90deg) translateX(0px) translateY(calc(var(--depth)* -0.5)) translateZ(calc(var(--height) - var(--depth)/2));
}
.cover {
width: 100%;
height: 100%;
object-fit: cover;
}
}
</style>