341 lines
8.2 KiB
Vue
341 lines
8.2 KiB
Vue
<template>
|
|
<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>
|
|
<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>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { Card } from '~~/types/types'
|
|
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, autoplay?: boolean }>()
|
|
const autoplay = props.autoplay ?? false
|
|
|
|
// State
|
|
const isLoadingTrack = ref(false)
|
|
const isFirstPlay = ref(true)
|
|
const progressPercentage = ref(0)
|
|
const currentTurns = ref(0)
|
|
const totalTurns = ref(0)// Refs pour les instances
|
|
|
|
// Refs pour les instances
|
|
const disc = ref<Disc | null>(null)
|
|
const sampler = ref<Sampler | null>(null)
|
|
const discRef = ref<HTMLElement>()
|
|
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) => {
|
|
// console.log('[INIT] Platine')
|
|
discRef.value = element
|
|
disc.value = new Disc(element)
|
|
sampler.value = new Sampler()
|
|
|
|
// Configurer les callbacks du disque
|
|
if (disc.value) {
|
|
disc.value.callbacks.onStop = () => {
|
|
// console.log('[DISC] On Stop')
|
|
sampler.value?.pause()
|
|
}
|
|
|
|
disc.value.callbacks.onDragStart = () => {
|
|
// 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.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) {
|
|
const safeSecondsPlayed = Math.max(0, secondsPlayed)
|
|
sampler.value?.updateSpeed(playbackSpeed, isReversed, safeSecondsPlayed)
|
|
}
|
|
updateTurns()
|
|
}
|
|
}
|
|
}
|
|
|
|
const updateTurns = () => {
|
|
if (!disc.value) return
|
|
|
|
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 (17% à 100%)
|
|
const minPercentage = 17
|
|
const maxPercentage = 100
|
|
const progressRatio = disc.value.secondsPlayed / (disc.value as any)._duration
|
|
const newProgressPercentage = minPercentage + (progressRatio * (maxPercentage - minPercentage))
|
|
|
|
// Ne mettre à jour que si les valeurs ont changé de manière significative
|
|
if (
|
|
Math.abs(currentTurns.value - newTurns) > 0.01 ||
|
|
Math.abs(totalTurns.value - newTotalTurns) > 0.01 ||
|
|
Math.abs(progressPercentage.value - newProgressPercentage) > 0.1
|
|
) {
|
|
currentTurns.value = newTurns
|
|
totalTurns.value = newTotalTurns
|
|
progressPercentage.value = newProgressPercentage
|
|
}
|
|
}
|
|
|
|
const loadCard = async (card: Card) => {
|
|
// console.log('[LOAD CARD]', card)
|
|
if (!sampler.value || !card) return
|
|
|
|
isLoadingTrack.value = true
|
|
// console.log(disc.value)
|
|
try {
|
|
await sampler.value.loadTrack(card.url_audio)
|
|
if (disc.value) {
|
|
// console.log('[DISC] Set Duration', sampler.value.duration)
|
|
disc.value.setDuration(sampler.value.duration)
|
|
updateTurns()
|
|
}
|
|
} finally {
|
|
isLoadingTrack.value = false
|
|
}
|
|
}
|
|
|
|
const play = (position = 0) => {
|
|
isFirstPlay.value = false
|
|
if (!disc.value || !sampler.value || !props.card) return
|
|
|
|
sampler.value.play(position)
|
|
disc.value.powerOn()
|
|
}
|
|
|
|
const pause = () => {
|
|
// console.log('[PAUSE]')
|
|
if (!disc.value || !sampler.value) return
|
|
|
|
sampler.value.pause()
|
|
disc.value.powerOff()
|
|
}
|
|
|
|
const togglePlay = () => {
|
|
if (isPlaying.value) {
|
|
pause()
|
|
} else {
|
|
// Reprendre la lecture à la position actuelle
|
|
const currentPosition = disc.value?.secondsPlayed || 0
|
|
play(currentPosition)
|
|
}
|
|
}
|
|
|
|
// Nettoyage
|
|
const cleanup = () => {
|
|
// console.log('[CLEANUP] Platine')
|
|
if (disc.value) {
|
|
disc.value.stop()
|
|
disc.value.powerOff()
|
|
}
|
|
if (sampler.value) {
|
|
sampler.value.pause()
|
|
}
|
|
}
|
|
|
|
const Power = (e: MouseEvent) => {
|
|
togglePlay()
|
|
}
|
|
|
|
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 () => {
|
|
if (!disc.value || !sampler.value) return
|
|
Reverse()
|
|
play()
|
|
|
|
}
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.code === 'Space' || e.key === ' ' || e.keyCode === 32) {
|
|
e.preventDefault() // Empêcher le défilement de la page
|
|
togglePlay()
|
|
}
|
|
}
|
|
|
|
// Initialisation du lecteur
|
|
onMounted(async () => {
|
|
isMounted.value = true
|
|
if (discRef.value) {
|
|
initPlatine(discRef.value)
|
|
await loadCard(props.card!)
|
|
if (autoplay) {
|
|
await nextTick()
|
|
play()
|
|
}
|
|
}
|
|
|
|
window.addEventListener('keydown', handleKeyDown)
|
|
})
|
|
|
|
// Nettoyage de l'écouteur d'événement
|
|
onUnmounted(() => {
|
|
window.removeEventListener('keydown', handleKeyDown)
|
|
isMounted.value = false
|
|
cleanup()
|
|
})
|
|
|
|
// Surveillance des changements de piste
|
|
watch(() => props.card, (newCard) => {
|
|
// console.log('[WATCH] Card', newCard)
|
|
if (newCard) {
|
|
loadCard(newCard)
|
|
}
|
|
})
|
|
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.platine {
|
|
overflow: hidden;
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
|
|
.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 {
|
|
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%);
|
|
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;
|
|
}
|
|
|
|
@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;
|
|
}
|
|
}
|
|
</style>
|