This commit is contained in:
		
							
								
								
									
										126
									
								
								app/components/molecule/atropos.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								app/components/molecule/atropos.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,126 @@ | ||||
| <template> | ||||
|   <div class="w-96 m-6"> | ||||
|     <atropos-component class="atropos game-box atropos-rotate-touch-scroll-y" rotate-touch="scroll-y" rotate-x-max="24" | ||||
|       rotate-y-max="24"> | ||||
|  | ||||
|       <div class="atropos-inner relative"> | ||||
|         <div class="game-box-bg bg-gradient-to-t from-slate-800 to-zinc-900 h-96 relative" data-atropos-offset="-8" /> | ||||
|         <img :src="id + '/object.png'" data-atropos-offset="-3" class="absolute bottom-0 inset-0 h-96 object-cover"> | ||||
|         <img :src="id + '/name.png'" data-atropos-offset="0" class="absolute inset-0 self-end justify-self-end p-4"> | ||||
|         <img src="/logo.svg" data-atropos-offset="0" width="70%" | ||||
|           class="logo absolute inset-0 self-center justify-self-center"> | ||||
|         <!-- <img src="/play.svg" width="20%" class="absolute play"> --> | ||||
|       </div> | ||||
|       <span class="game-box-t" /> | ||||
|       <span class="game-box-r" /> | ||||
|       <span class="game-box-b" /> | ||||
|       <span class="game-box-l" /> | ||||
|     </atropos-component> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| const id = 'ES01A' | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
| .game-box .logo { | ||||
|   filter: drop-shadow(4px 4px 0 rgb(0 0 0 / 0.8)); | ||||
| } | ||||
|  | ||||
| .game-box { | ||||
|   --side-color: #004297; | ||||
|   --side-size: 32px; | ||||
|   aspect-ratio: 526 / 656; | ||||
| } | ||||
|  | ||||
| .atropos-rotate { | ||||
|   position: relative; | ||||
| } | ||||
|  | ||||
| .game-box .atropos-rotate:before { | ||||
|   content: ""; | ||||
|   position: absolute; | ||||
|   left: 0; | ||||
|   width: calc(100% + 8px); | ||||
|   height: calc(100% + 16px); | ||||
|   top: -8px; | ||||
|   background: #086ef4; | ||||
|   z-index: 1; | ||||
| } | ||||
|  | ||||
| .atropos-inner { | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   overflow: hidden; | ||||
|   transform-style: preserve-3d; | ||||
|   transform: translateZ(0); | ||||
|   display: block; | ||||
|   z-index: 1; | ||||
|   position: relative; | ||||
| } | ||||
|  | ||||
| .game-box-t, | ||||
| .game-box-r, | ||||
| .game-box-b, | ||||
| .game-box-l { | ||||
|   transform-style: preserve-3d; | ||||
|   backface-visibility: hidden; | ||||
|   position: absolute; | ||||
|   /* display: none; */ | ||||
| } | ||||
|  | ||||
| .game-box-t { | ||||
|   width: calc(100% + 8px); | ||||
|   height: var(--side-size); | ||||
|   background: var(--side-color); | ||||
|   left: 0; | ||||
|   top: -8px; | ||||
|   transform: translate3d(0, 0, -32px) rotateX(90deg); | ||||
|   transform-origin: center top; | ||||
|   top: -8px; | ||||
|   transform: translateZ(-32px) rotateX(90deg); | ||||
|   transform-origin: center top; | ||||
| } | ||||
|  | ||||
| .game-box-b { | ||||
|   width: calc(100% + 8px); | ||||
|   height: var(--side-size); | ||||
|   background: var(--side-color); | ||||
|   left: 0; | ||||
|   bottom: -8px; | ||||
|   transform: translate3d(0, 0, -32px) rotateX(-90deg); | ||||
|   transform-origin: center bottom; | ||||
| } | ||||
|  | ||||
| .game-box-r { | ||||
|   width: var(--side-size); | ||||
|   height: calc(100% + 16px); | ||||
|   background: var(--side-color); | ||||
|   right: -8px; | ||||
|   top: -8px; | ||||
|   transform: translate3d(0, 0, -32px) rotateY(90deg); | ||||
|   transform-origin: right center; | ||||
| } | ||||
|  | ||||
| .game-box-l { | ||||
|   width: var(--side-size); | ||||
|   height: calc(100% + 16px); | ||||
|   background: var(--side-color); | ||||
|   left: 0px; | ||||
|   top: -8px; | ||||
|   transform: translate3d(0, 0, -32px) rotateY(-90deg); | ||||
|   transform-origin: left center; | ||||
|   overflow: hidden; | ||||
|  | ||||
|   &::before { | ||||
|     content: ''; | ||||
|     position: absolute; | ||||
|     width: 100%; | ||||
|     height: 10.4%; | ||||
|     background: #a5a5a5; | ||||
|     left: 0; | ||||
|     top: 9px; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										296
									
								
								app/components/molecule/box.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										296
									
								
								app/components/molecule/box.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,296 @@ | ||||
| <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' } | ||||
| ) | ||||
|  | ||||
| // --- 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 --- | ||||
| onMounted(() => { | ||||
|   applyColor() | ||||
|   applyBoxState() | ||||
|  | ||||
|   const down = (ev: PointerEvent) => { | ||||
|     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 } | ||||
|   } | ||||
|  | ||||
|   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 { scene.value?.releasePointerCapture(ev.pointerId) } catch { } | ||||
|     if (enableInertia && (Math.abs(velocity.x) > minVelocity || Math.abs(velocity.y) > minVelocity)) { | ||||
|       if (!raf) raf = requestAnimationFrame(tickInertia) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   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) | ||||
|  | ||||
|   onBeforeUnmount(() => { | ||||
|     cancelAnimationFrame(raf!) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| // --- Watchers --- | ||||
| watch(() => props.BoxState, () => applyBoxState()) | ||||
| watch(() => props.compilation, () => applyColor(), { deep: true }) | ||||
| </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); | ||||
|     width: var(--width); | ||||
|     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> | ||||
							
								
								
									
										25
									
								
								app/components/molecule/card.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/components/molecule/card.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| <template> | ||||
|   <article | ||||
|     class="backdrop-blur-sm -mt-12 z-10 card w-56 h-80 p-3 bg-opacity-10 bg-white rounded-2xl shadow-lg flex flex-col overflow-hidden"> | ||||
|     <!-- Cover --> | ||||
|     <figure class="flex-1 overflow-hidden rounded-t-xl"> | ||||
|       <img :src="coverUrl" alt="Pochette de l'album" class="w-full h-full object-cover object-center" /> | ||||
|     </figure> | ||||
|  | ||||
|     <!-- Body --> | ||||
|     <div class="p-3 text-center bg-white rounded-b-xl"> | ||||
|       <h2 class="text-base text-neutral-800 font-bold truncate">{{ props.track.title }}</h2> | ||||
|       <p class="text-sm text-neutral-500 truncate"> | ||||
|         {{ props.track.artist.name }} | ||||
|       </p> | ||||
|     </div> | ||||
|   </article> | ||||
|  | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import type { Track } from '~~/types/types' | ||||
|  | ||||
| const props = defineProps<{ track: Track }>() | ||||
| const coverUrl = `https://f4.bcbits.com/img/${props.track.artist.coverId}_4.jpg` | ||||
| </script> | ||||
							
								
								
									
										103
									
								
								app/components/molecule/theObject.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								app/components/molecule/theObject.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | ||||
| <template> | ||||
|   <h1 class="flex items-center justify-center min-h-screen"> | ||||
|     <atropos-component class="my-atropos"> | ||||
|       <span class="game-box-t" /> | ||||
|       <span class="game-box-r" /> | ||||
|       <span class="game-box-b" /> | ||||
|       <span class="game-box-l" /> | ||||
|       <div class="game-box-bg bg-gradient-to-t from-slate-800 to-zinc-900 h-60" data-atropos-offset="-8" /> | ||||
|       <img :src="id + '/object.png'" data-atropos-offset="-3" class="absolute inset-0 object-cover"> | ||||
|       <img :src="id + '/name.png'" data-atropos-offset="0" class="absolute inset-0 object-cover"> | ||||
|       <img src="/logo.svg" data-atropos-offset="0" width="70%" class="logo absolute inset-0"> | ||||
|       <img src="/play.svg" width="20%" class="absolute play"> | ||||
|     </atropos-component> | ||||
|   </h1> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| const id = 'ES01A' | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
| /* .atropos-inner, | ||||
| .game-box-bg { | ||||
|   width: 300px; | ||||
|   height: 300px; | ||||
| } */ | ||||
|  | ||||
| .game-box-t, | ||||
| .game-box-r, | ||||
| .game-box-b, | ||||
| .game-box-l { | ||||
|   transform-style: preserve-3d; | ||||
|   backface-visibility: hidden; | ||||
|   position: absolute; | ||||
|   /* display: none; */ | ||||
| } | ||||
|  | ||||
| .game-box-t { | ||||
|   width: calc(100% + 8px); | ||||
|   height: var(--side-size); | ||||
|   background: var(--side-color); | ||||
|   left: 0; | ||||
|   top: -8px; | ||||
|   transform: translate3d(0, 0, -32px) rotateX(90deg); | ||||
|   transform-origin: center top; | ||||
| } | ||||
|  | ||||
| .game-box-b { | ||||
|   width: calc(100% + 8px); | ||||
|   height: var(--side-size); | ||||
|   background: var(--side-color); | ||||
|   left: 0; | ||||
|   bottom: -8px; | ||||
|   transform: translate3d(0, 0, -32px) rotateX(-90deg); | ||||
|   transform-origin: center bottom; | ||||
| } | ||||
|  | ||||
| .game-box-r { | ||||
|   width: var(--side-size); | ||||
|   height: calc(100% + 16px); | ||||
|   background: var(--side-color); | ||||
|   right: -8px; | ||||
|   top: -8px; | ||||
|   transform: translate3d(0, 0, -32px) rotateY(90deg); | ||||
|   transform-origin: right center; | ||||
| } | ||||
|  | ||||
| .game-box-l { | ||||
|   width: var(--side-size); | ||||
|   height: calc(100% + 16px); | ||||
|   background: var(--side-color); | ||||
|   left: 0px; | ||||
|   top: -8px; | ||||
|   transform: translate3d(0, 0, -32px) rotateY(-90deg); | ||||
|   transform-origin: left center; | ||||
|   overflow: hidden; | ||||
|  | ||||
|   &::before { | ||||
|     content: ''; | ||||
|     position: absolute; | ||||
|     width: 100%; | ||||
|     height: 10.4%; | ||||
|     background: #a5a5a5; | ||||
|     left: 0; | ||||
|     top: 9px; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .atropos-rotate { | ||||
|   position: relative; | ||||
| } | ||||
|  | ||||
| .atropos-rotate:before { | ||||
|   content: ""; | ||||
|   position: absolute; | ||||
|   left: 0; | ||||
|   width: calc(100% + 8px); | ||||
|   height: calc(100% + 16px); | ||||
|   top: -8px; | ||||
|   background: #086ef4; | ||||
|   z-index: 1; | ||||
| } | ||||
| </style> | ||||
		Reference in New Issue
	
	Block a user