sql #33
@@ -6,10 +6,13 @@
|
|||||||
<b v-else>normal</b>
|
<b v-else>normal</b>
|
||||||
</button>
|
</button>
|
||||||
<button @click="Rewind">
|
<button @click="Rewind">
|
||||||
rewind
|
Rewind
|
||||||
|
</button>
|
||||||
|
<button @click="Wind">
|
||||||
|
Wind
|
||||||
</button>
|
</button>
|
||||||
<!-- <div>{{ progressPercentage }}</div> -->
|
<!-- <div>{{ progressPercentage }}</div> -->
|
||||||
<div>{{ currentSpeed }}</div>
|
<div>{{ (currentSpeed).toFixed(2) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="disc" ref="discRef" id="disc">
|
<div class="disc" ref="discRef" id="disc">
|
||||||
<div class="bobine" :style="{
|
<div class="bobine" :style="{
|
||||||
@@ -31,6 +34,7 @@ import type { Card } from '~~/types/types'
|
|||||||
import { ref, onMounted, onUnmounted, watch, computed, nextTick } from 'vue'
|
import { ref, onMounted, onUnmounted, watch, computed, nextTick } from 'vue'
|
||||||
import Disc from '~/utils/platine/disc'
|
import Disc from '~/utils/platine/disc'
|
||||||
import Sampler from '~/utils/platine/sampler'
|
import Sampler from '~/utils/platine/sampler'
|
||||||
|
import { duration } from 'drizzle-orm/gel-core';
|
||||||
|
|
||||||
const props = defineProps<{ card?: Card, autoplay?: boolean }>()
|
const props = defineProps<{ card?: Card, autoplay?: boolean }>()
|
||||||
const autoplay = props.autoplay ?? false
|
const autoplay = props.autoplay ?? false
|
||||||
@@ -184,9 +188,97 @@ const Reverse = () => {
|
|||||||
|
|
||||||
const Rewind = async () => {
|
const Rewind = async () => {
|
||||||
if (!disc.value || !sampler.value) return
|
if (!disc.value || !sampler.value) return
|
||||||
Reverse()
|
|
||||||
play()
|
|
||||||
|
|
||||||
|
// Sauvegarder l'état actuel
|
||||||
|
disc.value.isStopped() ? play() : null
|
||||||
|
const wasPlaying = !disc.value.isStopped()
|
||||||
|
const currentSpeed = disc.value.playbackSpeed
|
||||||
|
|
||||||
|
// Fonction pour l'effet de scratch/pull up
|
||||||
|
const scratchEffect = async () => {
|
||||||
|
// Ralentir progressivement
|
||||||
|
const slowDownDuration = 300 // ms
|
||||||
|
const startTime = performance.now()
|
||||||
|
const startSpeed = wasPlaying ? currentSpeed : 0.1
|
||||||
|
|
||||||
|
const slowDown = (timestamp: number) => {
|
||||||
|
const elapsed = timestamp - startTime
|
||||||
|
const progress = Math.min(elapsed / slowDownDuration, 1)
|
||||||
|
|
||||||
|
// Courbe d'accélération pour un effet plus naturel
|
||||||
|
const easeOut = 1 - Math.pow(1 - progress, 2)
|
||||||
|
|
||||||
|
// Ralentir progressivement jusqu'à presque l'arrêt
|
||||||
|
const newSpeed = startSpeed * (1 - easeOut) + 0.05
|
||||||
|
sampler.value?.setPlaybackRate(newSpeed)
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(slowDown)
|
||||||
|
} else {
|
||||||
|
// Une fois ralenti, effectuer le rembobinage
|
||||||
|
performRewind()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(slowDown)
|
||||||
|
}
|
||||||
|
|
||||||
|
const performRewind = () => {
|
||||||
|
// Mettre en pause la lecture actuelle
|
||||||
|
if (wasPlaying) {
|
||||||
|
sampler.value?.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remettre à zéro la position
|
||||||
|
sampler.value?.play(0)
|
||||||
|
disc.value?.setAngle(0)
|
||||||
|
|
||||||
|
// Effet de scratch rapide
|
||||||
|
const scratchDuration = 200 // ms
|
||||||
|
const scratchStart = performance.now()
|
||||||
|
|
||||||
|
const scratch = (timestamp: number) => {
|
||||||
|
const elapsed = timestamp - scratchStart
|
||||||
|
const progress = Math.min(elapsed / scratchDuration, 1)
|
||||||
|
|
||||||
|
// Créer un effet de scratch en variant la vitesse
|
||||||
|
if (progress < 0.5) {
|
||||||
|
// Phase de scratch vers l'arrière
|
||||||
|
const scratchProgress = progress * 2
|
||||||
|
const scratchSpeed = 1 - (scratchProgress * 1.8) // Ralenti jusqu'à -0.8
|
||||||
|
sampler.value?.setPlaybackRate(scratchSpeed)
|
||||||
|
} else {
|
||||||
|
// Phase de pull up
|
||||||
|
const pullProgress = (progress - 0.5) * 2
|
||||||
|
const pullSpeed = -0.8 + (pullProgress * 1.8) // Accélère de -0.8 à 1.0
|
||||||
|
sampler.value?.setPlaybackRate(pullSpeed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(scratch)
|
||||||
|
} else {
|
||||||
|
// Remettre la vitesse normale à la fin
|
||||||
|
sampler.value?.setPlaybackRate(1)
|
||||||
|
|
||||||
|
// Si c'était en lecture avant, relancer la lecture
|
||||||
|
if (wasPlaying) {
|
||||||
|
setTimeout(() => {
|
||||||
|
sampler.value?.play(0)
|
||||||
|
}, 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(scratch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Démarrer l'effet
|
||||||
|
scratchEffect()
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wind = () => {
|
||||||
|
disc.value.secondsPlayed = duration
|
||||||
|
sampler.value.secondsPlayed = duration
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
@@ -237,7 +329,7 @@ watch(() => props.card, (newCard) => {
|
|||||||
|
|
||||||
.disc {
|
.disc {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
position: fixed;
|
position: absolute;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
position: relative;
|
position: relative;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
|
|||||||
85
app/components/PlayingCard.vue
Normal file
85
app/components/PlayingCard.vue
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<section class="playing-card" :class="{ 'playing-card--platine-open': platineOpen }">
|
||||||
|
<Card :card="props.card!" :is-face-up="props.isFaceUp" @click="clickOnCard" :role="platineOpen ? 'img' : 'button'"
|
||||||
|
showPlayButtonFaceUp />
|
||||||
|
<Platine v-if="platineOpen && card" :card="card" autoplay />
|
||||||
|
<CloseButton v-if="platineOpen" @click="platineOpen = false" />
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Card } from '~~/types/types'
|
||||||
|
|
||||||
|
const props = defineProps<{ card: Card, isFaceUp: boolean }>()
|
||||||
|
|
||||||
|
const platineOpen = ref(false)
|
||||||
|
|
||||||
|
const clickOnCard = () => {
|
||||||
|
platineOpen.value = !platineOpen.value
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
$open-speed: 0.4s;
|
||||||
|
|
||||||
|
.playing-card {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
:deep(.card) {
|
||||||
|
transition: width $open-speed, height $open-speed, transform 0.1s;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
.face-up,
|
||||||
|
.pochette {
|
||||||
|
transition: border-radius $open-speed, box-shadow $open-speed;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
.play-button {
|
||||||
|
@apply scale-90;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--platine-open {
|
||||||
|
.card {
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
@apply z-auto scale-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card,
|
||||||
|
.platine {
|
||||||
|
width: 100vmin;
|
||||||
|
height: 100vmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:deep(.card) {
|
||||||
|
.face-up {
|
||||||
|
border-radius: 999px;
|
||||||
|
@apply shadow-xl;
|
||||||
|
|
||||||
|
.pochette {
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank,
|
||||||
|
.play-button {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-button,
|
||||||
|
.card-body,
|
||||||
|
.face-down {
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="screen-centered">
|
<PlayingCard :card :isFaceUp />
|
||||||
<Card :card="card" :isFaceUp="isFaceUp" showPlayButtonFaceUp @click="clickOnSlugCard" @image-loaded="imageLoaded" />
|
|
||||||
</section>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Card } from '~~/types/types'
|
import type { Card } from '~~/types/types'
|
||||||
|
|
||||||
|
const isFaceUp = ref(false)
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const slug = route.params.slug as string
|
const slug = route.params.slug as string
|
||||||
const isFaceUp = ref(false)
|
|
||||||
|
|
||||||
const { data: card, pending, error } = await useFetch<Card>(`/api/card/${slug}`)
|
const { data: card, pending, error } = await useFetch<Card>(`/api/card/${slug}`)
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
@@ -18,17 +16,9 @@ useHead({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const clickOnSlugCard = () => {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageLoaded = () => {
|
|
||||||
console.log('imageLoaded')
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isFaceUp.value = true
|
isFaceUp.value = true
|
||||||
}, 700)
|
}, 700)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -1,95 +1,58 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="platine-card" :class="{ 'platine-card--platine-open': platineOpen }">
|
<section class="deck">
|
||||||
<Card :card="card!" :is-face-up @click="clickOnCard" :role="platineOpen ? 'img' : 'button'" showPlayButtonFaceUp />
|
<div v-for="(card, index) in cards" :key="index">
|
||||||
<Platine v-if="platineOpen && card" :card="card" autoplay />
|
<PlayingCard :card="card" :isFaceUp="isFaceUp[index] || false" />
|
||||||
<CloseButton v-if="platineOpen" @click="platineOpen = false" />
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Card } from '~~/types/types'
|
import type { Card } from '~~/types/types'
|
||||||
|
|
||||||
const { data: card, pending, error } = await useFetch<Card>('/api/card/random')
|
const nbCards = 1 // Nombre de cartes à afficher
|
||||||
|
const cards = ref<Card[]>([])
|
||||||
|
const isFaceUp = ref<boolean[]>([])
|
||||||
|
|
||||||
const isFaceUp = ref(false)
|
// Chargement des cartes
|
||||||
const platineOpen = ref(false)
|
const loadCards = async () => {
|
||||||
|
try {
|
||||||
|
const promises = Array(nbCards).fill(0).map(() =>
|
||||||
|
$fetch<Card>('/api/card/random')
|
||||||
|
)
|
||||||
|
const results = await Promise.all(promises)
|
||||||
|
cards.value = results
|
||||||
|
isFaceUp.value = new Array(nbCards).fill(false)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading cards:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animation de retournement des cartes
|
||||||
|
const flipCards = () => {
|
||||||
|
isFaceUp.value.forEach((_, index) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
isFaceUp.value[index] = true
|
||||||
|
}, 400 * (index + 1))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadCards()
|
||||||
|
setTimeout(flipCards, 600)
|
||||||
|
})
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: computed(() =>
|
title: computed(() =>
|
||||||
card.value ? `${card.value.artist} - ${card.value.title}` : 'Loading...'
|
cards.value[0] ? `${cards.value[0].artist} - ${cards.value[0].title}` : 'Loading...'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
isFaceUp.value = true
|
|
||||||
}, 700)
|
|
||||||
})
|
|
||||||
|
|
||||||
const clickOnCard = () => {
|
|
||||||
platineOpen.value = !platineOpen.value
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style lang="scss" scoped>
|
||||||
$open-speed: 0.4s;
|
.deck {
|
||||||
|
|
||||||
.platine-card {
|
|
||||||
@apply screen-centered h-screen;
|
@apply screen-centered h-screen;
|
||||||
|
display: grid;
|
||||||
:deep(.card) {
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
transition: width $open-speed, height $open-speed, transform 0.1s;
|
grid-template-rows: 1fr 1fr 1fr;
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
.face-up,
|
|
||||||
.pochette {
|
|
||||||
transition: border-radius $open-speed, box-shadow $open-speed;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
.play-button {
|
|
||||||
@apply scale-90;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--platine-open {
|
|
||||||
.card {
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
@apply z-auto scale-100;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card,
|
|
||||||
.platine {
|
|
||||||
width: 100vmin;
|
|
||||||
height: 100vmin;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:deep(.card) {
|
|
||||||
.face-up {
|
|
||||||
border-radius: 999px;
|
|
||||||
@apply shadow-xl;
|
|
||||||
|
|
||||||
.pochette {
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank,
|
|
||||||
.play-button {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-button,
|
|
||||||
.card-body,
|
|
||||||
.face-down {
|
|
||||||
display: none;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
23
app/pages/random.vue
Normal file
23
app/pages/random.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<PlayingCard :card :isFaceUp />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Card } from '~~/types/types'
|
||||||
|
|
||||||
|
const { data: card, pending, error } = await useFetch<Card>('/api/card/random')
|
||||||
|
|
||||||
|
const isFaceUp = ref(false)
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: computed(() =>
|
||||||
|
card.value ? `${card.value.artist} - ${card.value.title}` : 'Loading...'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
isFaceUp.value = true
|
||||||
|
}, 700)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -77,7 +77,7 @@ class Disc {
|
|||||||
private _inertiaFriction: number = 1 // Coefficient de frottement pour l'inertie (plus proche de 1 = plus long)
|
private _inertiaFriction: number = 1 // Coefficient de frottement pour l'inertie (plus proche de 1 = plus long)
|
||||||
private _lastDragVelocity: number = 0 // Dernière vitesse de drag
|
private _lastDragVelocity: number = 0 // Dernière vitesse de drag
|
||||||
private _lastDragTime: number = 0 // Dernier temps de drag
|
private _lastDragTime: number = 0 // Dernier temps de drag
|
||||||
private _inertiaAmplification: number = 45 // Facteur d'amplification de l'inertie
|
private _inertiaAmplification: number = 25 // Facteur d'amplification de l'inertie
|
||||||
private _previousDuration: number = 0 // Pour suivre les changements de durée
|
private _previousDuration: number = 0 // Pour suivre les changements de durée
|
||||||
|
|
||||||
public isReversed: boolean = false
|
public isReversed: boolean = false
|
||||||
@@ -131,7 +131,7 @@ class Disc {
|
|||||||
this._draggingSpeeds = limit(this._draggingSpeeds, 10)
|
this._draggingSpeeds = limit(this._draggingSpeeds, 10)
|
||||||
|
|
||||||
this._playbackSpeed = average(this._draggingSpeeds)
|
this._playbackSpeed = average(this._draggingSpeeds)
|
||||||
this._playbackSpeed = clamp(this._playbackSpeed, -4, 4)
|
// this._playbackSpeed = clamp(this._playbackSpeed, -4, 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
get secondsPlayed() {
|
get secondsPlayed() {
|
||||||
@@ -354,11 +354,11 @@ class Disc {
|
|||||||
this._inertiaVelocity = 0
|
this._inertiaVelocity = 0
|
||||||
this._isInertiaActive = false
|
this._isInertiaActive = false
|
||||||
this._playbackSpeed = 0
|
this._playbackSpeed = 0
|
||||||
this._basePlaybackSpeed = 0
|
this._basePlaybackSpeed = 1
|
||||||
this.el.style.transform = 'rotate(0rad)'
|
this.el.style.transform = 'rotate(0rad)'
|
||||||
this.callbacks.onStop()
|
// this.callbacks.onStop()
|
||||||
this._isPoweredOn = false
|
// this._isPoweredOn = false
|
||||||
this.stop()
|
// this.stop()
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
// Arrêt à la fin (angle >= _maxAngle)
|
// Arrêt à la fin (angle >= _maxAngle)
|
||||||
|
|||||||
@@ -132,6 +132,24 @@ class Sampler {
|
|||||||
unmute() {
|
unmute() {
|
||||||
this.gainNode.gain.value = 1
|
this.gainNode.gain.value = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Définit le taux de lecture (vitesse de lecture)
|
||||||
|
* @param rate Taux de lecture (1.0 = vitesse normale, 0.5 = moitié de vitesse, 2.0 = double vitesse, etc.)
|
||||||
|
*/
|
||||||
|
setPlaybackRate(rate: number) {
|
||||||
|
if (!this.audioSource) return
|
||||||
|
|
||||||
|
const currentTime = this.audioContext.currentTime
|
||||||
|
this.audioSource.playbackRate.cancelScheduledValues(currentTime)
|
||||||
|
this.audioSource.playbackRate.linearRampToValueAtTime(
|
||||||
|
Math.max(0.001, Math.abs(rate)), // Éviter les valeurs négatives ou nulles
|
||||||
|
currentTime + 0.05 // Petit délai pour éviter les clics
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mettre à jour la vitesse actuelle
|
||||||
|
this.currentSpeed = rate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Sampler
|
export default Sampler
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ module.exports = {
|
|||||||
'.debug': {
|
'.debug': {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
'z-index': '1000',
|
'z-index': '1000',
|
||||||
top: '16px',
|
bottom: '16px',
|
||||||
right: '16px',
|
right: '25%',
|
||||||
background: '#9CA3AF',
|
background: '#9CA3AF',
|
||||||
'border-radius': '16px',
|
'border-radius': '16px',
|
||||||
padding: '16px'
|
padding: '16px'
|
||||||
|
|||||||
Reference in New Issue
Block a user