WIP starbook demo
All checks were successful
Deploy App / build (push) Successful in 34s
Deploy App / deploy (push) Successful in 25s

This commit is contained in:
valere
2026-02-10 07:31:31 +01:00
parent 7fa6f6ccc8
commit 7be09dd12d
17 changed files with 516 additions and 914 deletions

View File

@@ -1,49 +1,43 @@
<template>
<div class="platine" :class="{ 'loading': isLoadingCard, 'mounted': isMounted }" ref="platine">
<div class="platine" :class="{ 'loading': isLoadingTrack, 'mounted': isMounted, 'playing': isPlaying }" ref="platine">
<div v-if="true" class="debug">
<button @click="Reverse">
<b v-if="isReversed">reversed</b>
<b v-else>normal</b>
</button>
<button @click="Rewind">
rewind
</button>
<div>{{ progressPercentage }}</div>
<div>{{ currentSpeed }}</div>
</div>
<div class="disc" ref="discRef" id="disc">
<div class="bobine" :style="{
height: progressPercentage + '%',
width: progressPercentage + '%'
}"></div>
<div class="power-button" @mousedown.stop @click.stop="handlePowerButtonClick">
<img class="power-logo" src="/favicon.svg">
<div class="power-loading" v-if="isLoadingCard">
<div class="spinner"></div>
</div>
<button class="power-button" @click="Power" @touchstart="Power" :disabled="isLoadingTrack">
<img class="macaron" src="/favicon.svg">
<div class="spinner" v-if="isLoadingTrack" />
</button>
<div class="turn-point" v-if="!isLoadingTrack">
</div>
<div class="turn-point" v-if="!isLoadingCard">
</div>
</div>
<div class="debug">
<b>progressPercentage</b>: <br>
</br>{{ Math.round(progressPercentage) }}%
<br>
<b>Disc</b>: <br>
</br>{{ Math.round(disc?.secondsPlayed || 0) }}sec
<br>
</br>{{ Math.round(sampler?.duration || 0) }}sec
<br>
<!-- <pre>{{ sampler }}</pre> -->
isPlaying:
</br>{{ isPlaying }}
<br>
<b>isFirstDrag</b>: <br>
</br>{{ isFirstDrag }}
</div>
</div>
</template>
<script setup lang="ts">
import type { Card } from '~~/types/types'
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
import { ref, onMounted, onUnmounted, watch, computed, nextTick } from 'vue'
import Disc from '~/utils/platine/disc'
import Sampler from '~/utils/platine/sampler'
const props = defineProps<{ card?: Card }>()
const props = defineProps<{ card?: Card, autoplay?: boolean }>()
const autoplay = props.autoplay ?? false
// State
const isLoadingCard = ref(false)
const isFirstDrag = ref(true)
const isLoadingTrack = ref(false)
const isFirstPlay = ref(true)
const progressPercentage = ref(0)
const currentTurns = ref(0)
const totalTurns = ref(0)// Refs pour les instances
@@ -56,6 +50,8 @@ const platine = ref<HTMLElement>()
const isMounted = ref(false)
const isPlaying = computed(() => Math.abs(Math.round(sampler.value?.currentSpeed || 0)) !== 0)
const isReversed = computed(() => disc.value?.isReversed || false)
const currentSpeed = computed(() => sampler.value?.currentSpeed || 0)
// Actions
const initPlatine = (element: HTMLElement) => {
@@ -72,23 +68,13 @@ const initPlatine = (element: HTMLElement) => {
}
disc.value.callbacks.onDragStart = () => {
// console.log('[DISC] On Drag Start')
if (isFirstDrag.value) {
isFirstDrag.value = false
// togglePlay()
if (sampler.value && disc.value) {
sampler.value.play(disc.value.secondsPlayed)
disc.value.powerOn()
// console.log('[DISC] Power ON')
}
// Activer le son à chaque fois qu'on glisse, pas seulement au premier play
if (sampler.value && disc.value) {
// On joue toujours le son quand on glisse, même après une pause
sampler.value.play(disc.value.secondsPlayed || 0)
}
}
disc.value.callbacks.onDragEnded = () => {
// console.log('[DISC] On Drag END')
sampler.value?.play(disc.value?.secondsPlayed || 0)
}
disc.value.callbacks.onLoop = ({ playbackSpeed, isReversed, secondsPlayed }) => {
// Ne mettre à jour que si nécessaire et s'assurer que la position est valide
if (Math.abs((sampler.value?.currentSpeed || 0) - playbackSpeed) > 0.01) {
@@ -106,10 +92,11 @@ const updateTurns = () => {
const newTurns = disc.value.secondsPlayed * 0.75
const newTotalTurns = (disc.value as any)._duration * 0.75
// Calcul du pourcentage de progression pour l'affichage visuel (22% à 100%)
const minPercentage = 22
// Calcul du pourcentage de progression pour l'affichage visuel (17% à 100%)
const minPercentage = 17
const maxPercentage = 100
const progressRatio = disc.value.secondsPlayed / (disc.value as any)._duration
const newProgressPercentage = minPercentage + (progressRatio * (100 - minPercentage))
const newProgressPercentage = minPercentage + (progressRatio * (maxPercentage - minPercentage))
// Ne mettre à jour que si les valeurs ont changé de manière significative
if (
@@ -127,7 +114,7 @@ const loadCard = async (card: Card) => {
// console.log('[LOAD CARD]', card)
if (!sampler.value || !card) return
isLoadingCard.value = true
isLoadingTrack.value = true
// console.log(disc.value)
try {
await sampler.value.loadTrack(card.url_audio)
@@ -137,12 +124,12 @@ const loadCard = async (card: Card) => {
updateTurns()
}
} finally {
isLoadingCard.value = false
isLoadingTrack.value = false
}
}
const play = (position = 0) => {
// console.log('[PLAY]')
isFirstPlay.value = false
if (!disc.value || !sampler.value || !props.card) return
sampler.value.play(position)
@@ -179,11 +166,24 @@ const cleanup = () => {
}
}
const handlePowerButtonClick = (e: MouseEvent) => {
e.stopPropagation() // Empêcher la propagation de l'événement
e.preventDefault() // Empêcher tout comportement par défaut
const Power = (e: MouseEvent) => {
togglePlay()
return false // Empêcher tout autre comportement
}
const Reverse = () => {
if (!disc.value || !sampler.value) return
// Sauvegarder la position actuelle
const currentPosition = disc.value.secondsPlayed || 0
const wasPlaying = !disc.value.isStopped()
// Inverser la direction du disque et du sampler
disc.value.reverse()
sampler.value.reverse(wasPlaying ? currentPosition : 0)
}
const Rewind = async () => {
// ...
}
const handleKeyDown = (e: KeyboardEvent) => {
@@ -194,14 +194,17 @@ const handleKeyDown = (e: KeyboardEvent) => {
}
// Initialisation du lecteur
onMounted(() => {
onMounted(async () => {
isMounted.value = true
if (discRef.value) {
initPlatine(discRef.value)
loadCard(props.card!)
await loadCard(props.card!)
if (autoplay) {
await nextTick()
play()
}
}
// Ajouter l'écouteur d'événement clavier
window.addEventListener('keydown', handleKeyDown)
})
@@ -224,134 +227,111 @@ watch(() => props.card, (newCard) => {
<style lang="scss">
.platine {
pointer-events: none;
overflow: hidden;
position: relative;
width: 100%;
height: 100%;
padding: 20px;
}
.disc {
pointer-events: auto;
position: fixed;
background-color: transparent;
position: relative;
aspect-ratio: 1;
width: 100%;
overflow: hidden;
border-radius: 50%;
cursor: grab;
background-position: center;
background-size: cover;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.4);
.disc {
pointer-events: auto;
position: fixed;
background-color: transparent;
position: relative;
aspect-ratio: 1;
width: 100%;
overflow: hidden;
border-radius: 50%;
cursor: grab;
background-position: center;
background-size: cover;
padding: 14px;
.loading & {
box-shadow: none;
}
}
.turn-point {
@apply absolute top-1/2 right-8 size-1/12 rounded-full bg-esyellow;
}
.disc.is-scratching {
cursor: grabbing;
}
.power-button {
border-radius: 999px;
background-size: cover;
background-position: center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
justify-content: center;
align-items: center;
text-align: center;
width: 45%;
aspect-ratio: 1/1;
// background: no-repeat url(/favicon.svg) center center;
background-size: 30%;
border-radius: 50%;
cursor: pointer !important;
.power-logo {
@apply size-1/2 bg-black rounded-full p-5;
.loading & {
box-shadow: none;
}
}
.power-loading {
.turn-point {
@apply absolute top-1/2 right-8 size-1/12 rounded-full bg-esyellow;
}
.disc.is-scratching {
cursor: grabbing;
}
.power-button {
position: absolute;
z-index: 100;
cursor: pointer;
transition: all 0.1s;
display: flex;
justify-content: center;
align-items: center;
transform-origin: center;
width: 33%;
height: 33%;
border-radius: 999px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
.macaron {
position: absolute;
filter: grayscale(1);
transition: transform 0.1s, filter 0.8s;
@apply size-2/3;
}
.spinner {
@apply size-1/2;
}
&:active {
.macaron {
transform: scale(0.8);
}
}
}
&.playing .power-button .macaron {
filter: grayscale(0);
}
.disc-middle {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
width: 20px;
height: 20px;
background: rgb(26, 26, 26);
border-radius: 50%;
}
}
.disc-middle {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
background: rgb(26, 26, 26);
border-radius: 50%;
}
.spinner {
width: 40px;
height: 40px;
border: 1px solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
// @apply md:border-8;
}
.button {
border-radius: 0;
border: none;
background: rgb(69, 69, 69);
font-size: 0.75rem;
padding: 0.4rem;
color: #fff;
line-height: 1.3;
cursor: pointer;
will-change: box-shadow;
transition:
box-shadow 0.2s ease-out,
transform 0.05s ease-in;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.power.is-active {
transform: translate(1px, 2px);
color: red;
}
.button[disabled] {
opacity: 0.5;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
.bobine {
width: 17%;
height: 17%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
@apply relative bg-black bg-opacity-30 backdrop-blur-sm rounded-full;
}
}
.bobine {
@apply bg-slate-900 bg-opacity-50 backdrop-blur absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full;
}
.debug {
@apply fixed top-4 right-4 bg-slate-200 rounded-md p-2;
}
</style>