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

@@ -74,16 +74,17 @@ class Disc {
private _inertiaVelocity: number = 0
private _isInertiaActive: boolean = false
private _basePlaybackSpeed: number = 1 // Vitesse de lecture normale
private _inertiaFriction: number = 0.93 // 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 _lastDragTime: number = 0 // Dernier temps de drag
private _inertiaAmplification: number = 25 // Facteur d'amplification de l'inertie
private _inertiaAmplification: number = 45 // Facteur d'amplification de l'inertie
private _previousDuration: number = 0 // Pour suivre les changements de durée
public isReversed: boolean = false
public callbacks = {
onDragStart: (): void => {},
onDragProgress: (): void => {},
onDragEnded: (secondsPlayed: number): void => {},
onStop: (): void => {},
onLoop: (params: DiscProgress): void => {}
@@ -147,12 +148,9 @@ class Disc {
}
powerOn() {
if (!this.rafId) {
this.start()
}
this._isPoweredOn = true
this._basePlaybackSpeed = 1
this._playbackSpeed = 1
this._basePlaybackSpeed = this.isReversed ? -1 : 1
this.start()
}
powerOff() {
@@ -250,15 +248,18 @@ class Disc {
const anglePointerToCenter = angleBetween(this._center, pointerPosition)
const angle_DraggingFromToCenter = angleBetween(this._center, this._draggingFrom)
const angleDragged = angleDifference(angle_DraggingFromToCenter, anglePointerToCenter)
let angleDragged = angleDifference(angle_DraggingFromToCenter, anglePointerToCenter)
// Calcul de la vitesse de déplacement angulaire (radians par milliseconde)
// On inverse le signe pour que le sens de l'inertie soit naturel
// Le signe est inversé pour que le glissement vers la droite fasse tourner vers la droite
if (deltaTime > 0) {
this._lastDragVelocity = -angleDragged / deltaTime
}
this._lastDragTime = currentTime
// Appliquer la rotation au disque
// Le signe est inversé pour que le glissement vers la droite fasse tourner vers la droite
this.setAngle(this._currentAngle - angleDragged)
this._draggingFrom = { ...pointerPosition }
}
@@ -269,12 +270,15 @@ class Disc {
// Activer l'inertie avec la vitesse de drag actuelle
this._isInertiaActive = true
// Augmenter la sensibilité du drag avec le facteur d'amplification
this._inertiaVelocity = this._lastDragVelocity * this._inertiaAmplification
// Ajuster la direction de l'inertie en fonction du mode reverse
const direction = this.isReversed ? -1 : 1
this._inertiaVelocity = this._lastDragVelocity * this._inertiaAmplification * direction
this.isDragging = false
// Toujours conserver la vitesse de base actuelle (1 si allumé, 0 si éteint)
this._basePlaybackSpeed = this._isPoweredOn ? 1 : 0
this._basePlaybackSpeed = this._isPoweredOn ? (this.isReversed ? -1 : 1) : 0
// Si le lecteur est éteint, s'assurer que la vitesse de base est bien à 0
if (!this._isPoweredOn) {
@@ -286,25 +290,41 @@ class Disc {
autoRotate(currentTimestamp: number) {
const timestampElapsed = currentTimestamp - this.previousTimestamp
const direction = this.isReversed ? -1 : 1
// Vérifier si on est au début du morceau en mode reverse
if (this.isReversed && this.secondsPlayed <= 0) {
this._currentAngle = 0
this._inertiaVelocity = 0
this._isInertiaActive = false
this._playbackSpeed = 0
this._basePlaybackSpeed = 0 // Arrêter complètement la lecture
this.el.style.transform = 'rotate(0rad)'
this.callbacks.onStop()
// Éteindre le lecteur pour éviter toute reprise automatique
this._isPoweredOn = false
this.stop()
return
}
if (this._isInertiaActive) {
// Appliquer l'inertie
const inertiaRotation = this._inertiaVelocity * timestampElapsed
this.setAngle(this._currentAngle + inertiaRotation)
// Appliquer l'inertie en tenant compte de la direction
const inertiaRotation = this._inertiaVelocity * timestampElapsed * direction
this.setAngle(this._currentAngle + inertiaRotation, true)
// Si le lecteur est allumé, faire une transition fluide vers la vitesse de lecture
if (this._isPoweredOn) {
const targetVelocity =
RADIANS_PER_MILLISECOND * Math.abs(this._basePlaybackSpeed) * direction
// Si on est proche de la vitesse de lecture normale, on désactive l'inertie
if (
Math.abs(this._inertiaVelocity - RADIANS_PER_MILLISECOND * this._basePlaybackSpeed) <
0.0001
) {
if (Math.abs(this._inertiaVelocity - targetVelocity) < 0.0001) {
this._isInertiaActive = false
this._inertiaVelocity = RADIANS_PER_MILLISECOND * this._basePlaybackSpeed
this._inertiaVelocity = targetVelocity
} else {
// Réduire progressivement la vitesse d'inertie vers la vitesse de lecture
this._inertiaVelocity +=
(RADIANS_PER_MILLISECOND * this._basePlaybackSpeed - this._inertiaVelocity) * 0.1
this._inertiaVelocity += (targetVelocity - this._inertiaVelocity) * 0.1
}
} else {
// Si le lecteur est éteint, appliquer un frottement normal
@@ -314,18 +334,61 @@ class Disc {
if (Math.abs(this._inertiaVelocity) < 0.0001) {
this._isInertiaActive = false
this._inertiaVelocity = 0
this._playbackSpeed = 0 // Mettre à jour la vitesse de lecture à 0 uniquement à la fin
this._playbackSpeed = 0
}
}
} else {
// Rotation normale à la vitesse de lecture de base
const baseRotation = RADIANS_PER_MILLISECOND * this._basePlaybackSpeed * timestampElapsed
this.setAngle(this._currentAngle + baseRotation)
// Rotation normale à la vitesse de lecture de base, dans la direction actuelle
const baseRotation =
RADIANS_PER_MILLISECOND * Math.abs(this._basePlaybackSpeed) * timestampElapsed * direction
this.setAngle(this._currentAngle + baseRotation, true)
}
}
setAngle(angle: number) {
this._currentAngle = clamp(angle, 0, this._maxAngle)
setAngle(angle: number, checkBounds = false) {
// Vérifier les limites si demandé
if (checkBounds) {
// Arrêt au début (angle < 0)
if (angle < 0) {
this._currentAngle = 0
this._inertiaVelocity = 0
this._isInertiaActive = false
this._playbackSpeed = 0
this._basePlaybackSpeed = 0
this.el.style.transform = 'rotate(0rad)'
this.callbacks.onStop()
this._isPoweredOn = false
this.stop()
return 0
}
// Arrêt à la fin (angle >= _maxAngle)
else if (angle >= this._maxAngle) {
this._currentAngle = this._maxAngle
this._inertiaVelocity = 0
this._isInertiaActive = false
this._playbackSpeed = 0
this._basePlaybackSpeed = 0
this.el.style.transform = `rotate(${this._maxAngle}rad)`
this.callbacks.onStop()
this._isPoweredOn = false
this.stop()
return this._maxAngle
}
}
// Si on dépasse les limites, on reste aux bornes
if (angle < 0) {
this._currentAngle = 0
} else if (angle > this._maxAngle) {
this._currentAngle = this._maxAngle
} else {
this._currentAngle = angle
}
// Appliquer la rotation à l'élément
if (this.el) {
this.el.style.transform = `rotate(${this._currentAngle}rad)`
}
return this._currentAngle
}
@@ -337,15 +400,43 @@ class Disc {
}
stop() {
if (this.rafId) {
cancelAnimationFrame(this.rafId)
this.rafId = null
}
this.callbacks.onStop()
this._isInertiaActive = false
this._inertiaVelocity = 0
cancelAnimationFrame(this.rafId!)
this.rafId = null
}
rewind() {
this.setAngle(0)
/**
* Vérifie si le disque est à l'arrêt
*/
isStopped(): boolean {
return this.rafId === null && !this._isInertiaActive
}
/**
* Inverse la direction de rotation du disque
* @returns true si l'inversion a réussi, false sinon
*/
reverse(): boolean {
if (!this.el) return false
// Inverser la direction
this.isReversed = !this.isReversed
// Inverser la vitesse de base si nécessaire
if (this._isPoweredOn) {
this._basePlaybackSpeed = this.isReversed ? -1 : 1
}
// Mettre à jour la direction de l'animation
this.el.style.animationDirection = this.isReversed ? 'reverse' : 'normal'
// Inverser la vitesse d'inertie si elle est active
if (this._isInertiaActive) {
this._inertiaVelocity = -this._inertiaVelocity
}
return true
}
loop() {
@@ -357,11 +448,18 @@ class Disc {
this.autoRotate(currentTimestamp)
}
// Calculer la vitesse de lecture
const rotated = this._currentAngle - this._previousAngle
const rotationNormal = RADIANS_PER_MILLISECOND * timestampDifferenceMS
this.playbackSpeed = rotated / rotationNormal || 0
this.isReversed = this._currentAngle < this._previousAngle
// Calculer la vitesse de lecture uniquement pendant le glissement ou l'inertie
if (this.isDragging || this._isInertiaActive) {
const rotated = this._currentAngle - this._previousAngle
const rotationNormal = RADIANS_PER_MILLISECOND * timestampDifferenceMS
this.playbackSpeed = rotated / rotationNormal || 0
} else if (this._isPoweredOn) {
// En mode lecture normale, utiliser la vitesse de base
this._playbackSpeed = this._basePlaybackSpeed
} else {
// Si le lecteur est éteint, vitesse à 0
this._playbackSpeed = 0
}
// Mettre à jour la rotation visuelle
this.el.style.transform = `rotate(${this._currentAngle}rad)`
@@ -373,7 +471,7 @@ class Disc {
// Ne pas appeler onLoop si rien n'a changé
if (this._previousAngle !== this._currentAngle || this._previousDuration !== this._duration) {
this.callbacks.onLoop({
playbackSpeed: this.playbackSpeed,
playbackSpeed: this._playbackSpeed, // Utiliser _playbackSpeed directement
isReversed: this.isReversed,
secondsPlayed,
progress

View File

@@ -105,6 +105,26 @@ class Sampler {
this.audioSource.stop()
}
/**
* Inverse la direction de lecture
* @returns La nouvelle direction (true = inversé, false = normal)
*/
reverse(secondsPlayed: number = 0): boolean {
if (!this.audioBuffer) return false
// Inverser la direction
this.isReversed = !this.isReversed
// Si on a une position, on relance la lecture à cette position
if (secondsPlayed > 0) {
// S'assurer que la position est dans les limites
const safePosition = Math.max(0, Math.min(secondsPlayed, this.duration))
this.play(safePosition)
}
return this.isReversed
}
mute() {
this.gainNode.gain.value = 0
}