Add isPlaying state to MusicSlider
This commit is contained in:
parent
e633a5e004
commit
33a1715adc
@ -48,6 +48,7 @@ export const Game = () => {
|
||||
|
||||
const [allSongs, setAllSongs] = useState([]);
|
||||
const [songsLoading, setSongsLoading] = useState(false);
|
||||
const [composerIsPlaying, setComposerIsPlaying] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!connected) return;
|
||||
@ -129,6 +130,7 @@ export const Game = () => {
|
||||
if (phase === "composing") {
|
||||
console.log("Received frequency update:", data.frequency);
|
||||
setFrequency(data.frequency);
|
||||
setComposerIsPlaying(data.isPlaying); // Make sure isPlaying is handled
|
||||
}
|
||||
},
|
||||
"phase-changed": (data) => {
|
||||
@ -223,10 +225,11 @@ export const Game = () => {
|
||||
}
|
||||
}, [allSongs, songOptions]);
|
||||
|
||||
const handleFrequencyChange = useCallback((newFrequency) => {
|
||||
const handleFrequencyChange = useCallback((newFrequency, isPlaying) => {
|
||||
setFrequency(newFrequency);
|
||||
setComposerIsPlaying(isPlaying);
|
||||
if (role === "composer") {
|
||||
send("submit-frequency", { frequency: newFrequency });
|
||||
send("submit-frequency", { frequency: newFrequency, isPlaying });
|
||||
}
|
||||
}, [role, send]);
|
||||
|
||||
@ -267,22 +270,10 @@ export const Game = () => {
|
||||
setTimeLeft(0);
|
||||
}, [send]);
|
||||
|
||||
const handlePlayerReady = useCallback((player) => {
|
||||
const handlePlayerReady = useCallback(() => {
|
||||
console.log("Player ready");
|
||||
}, []);
|
||||
|
||||
const togglePlayback = useCallback(() => {
|
||||
const audioElement = document.querySelector('audio');
|
||||
|
||||
if (audioElement) {
|
||||
if (audioElement.paused) {
|
||||
audioElement.play().catch(err => console.error("Play error:", err));
|
||||
} else {
|
||||
audioElement.pause();
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderPhaseContent = () => {
|
||||
switch (phase) {
|
||||
case "waiting":
|
||||
@ -543,6 +534,7 @@ export const Game = () => {
|
||||
isReadOnly={role !== "composer"}
|
||||
onFrequencyChange={handleFrequencyChange}
|
||||
frequency={frequency}
|
||||
composerIsPlaying={composerIsPlaying}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
@ -1,224 +1,217 @@
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import "./styles.sass";
|
||||
|
||||
export const MusicSlider = ({ isReadOnly = false, onFrequencyChange, frequency: externalFrequency }) => {
|
||||
const [frequency, setFrequency] = useState(externalFrequency || 440);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const audioContextRef = useRef(null);
|
||||
const oscillatorRef = useRef(null);
|
||||
const gainNodeRef = useRef(null);
|
||||
const sliderRef = useRef(null);
|
||||
const hasInteractedRef = useRef(false);
|
||||
export const MusicSlider = ({ isReadOnly, onFrequencyChange, frequency: externalFrequency, composerIsPlaying }) => {
|
||||
const [audioContext, setAudioContext] = useState(null);
|
||||
const [oscillator, setOscillator] = useState(null);
|
||||
const [gainNode, setGainNode] = useState(null);
|
||||
const sliderRef = useRef(null);
|
||||
const frequency = useRef(externalFrequency || 440);
|
||||
const isPressed = useRef(false);
|
||||
const isDragging = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const initAudioContext = () => {
|
||||
if (!audioContextRef.current && !hasInteractedRef.current) {
|
||||
hasInteractedRef.current = true;
|
||||
audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)();
|
||||
if (!oscillatorRef.current) {
|
||||
startAudio();
|
||||
}
|
||||
document.removeEventListener('click', initAudioContext);
|
||||
document.removeEventListener('touchstart', initAudioContext);
|
||||
document.removeEventListener('keydown', initAudioContext);
|
||||
}
|
||||
};
|
||||
const initAudio = useCallback(() => {
|
||||
if (audioContext) return;
|
||||
|
||||
document.addEventListener('click', initAudioContext);
|
||||
document.addEventListener('touchstart', initAudioContext);
|
||||
document.addEventListener('keydown', initAudioContext);
|
||||
try {
|
||||
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', initAudioContext);
|
||||
document.removeEventListener('touchstart', initAudioContext);
|
||||
document.removeEventListener('keydown', initAudioContext);
|
||||
};
|
||||
}, []);
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(frequency.current, ctx.currentTime);
|
||||
|
||||
useEffect(() => {
|
||||
if (externalFrequency !== undefined && !dragging) {
|
||||
setFrequency(externalFrequency);
|
||||
gain.gain.setValueAtTime(0.00001, ctx.currentTime);
|
||||
|
||||
if (audioContextRef.current) {
|
||||
if (!isPlaying) {
|
||||
startAudio();
|
||||
} else if (oscillatorRef.current) {
|
||||
oscillatorRef.current.frequency.setValueAtTime(
|
||||
externalFrequency,
|
||||
audioContextRef.current.currentTime
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [externalFrequency, dragging, isPlaying]);
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.start();
|
||||
|
||||
setAudioContext(ctx);
|
||||
setOscillator(osc);
|
||||
setGainNode(gain);
|
||||
|
||||
if (isReadOnly) {
|
||||
gain.gain.setValueAtTime(0.00001, ctx.currentTime);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Audio initialization error:", error);
|
||||
}
|
||||
}, [audioContext, isReadOnly]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isReadOnly || !externalFrequency) return;
|
||||
|
||||
// Initialize audio if not already done
|
||||
if (!audioContext) {
|
||||
initAudio();
|
||||
return;
|
||||
}
|
||||
|
||||
if (oscillator && gainNode) {
|
||||
frequency.current = externalFrequency;
|
||||
oscillator.frequency.setValueAtTime(frequency.current, audioContext.currentTime);
|
||||
|
||||
if (composerIsPlaying) {
|
||||
gainNode.gain.setTargetAtTime(0.5, audioContext.currentTime, 0.01);
|
||||
} else {
|
||||
gainNode.gain.setTargetAtTime(0.00001, audioContext.currentTime, 0.05);
|
||||
}
|
||||
}
|
||||
}, [externalFrequency, composerIsPlaying, oscillator, audioContext, gainNode, isReadOnly, initAudio]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isReadOnly) return;
|
||||
|
||||
const calculateFrequency = (clientX) => {
|
||||
const slider = sliderRef.current;
|
||||
const rect = slider.getBoundingClientRect();
|
||||
const width = rect.width;
|
||||
|
||||
// Calculate relative X position using window coordinates
|
||||
let x = clientX - rect.left;
|
||||
|
||||
// For positions outside the slider, calculate relative to slider bounds
|
||||
if (clientX < rect.left) x = 0;
|
||||
if (clientX > rect.right) x = width;
|
||||
|
||||
const percentage = x / width;
|
||||
return Math.round(220 + percentage * 880);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (!isPressed.current) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const newFreq = calculateFrequency(e.clientX);
|
||||
frequency.current = newFreq;
|
||||
onFrequencyChange(newFreq, true);
|
||||
|
||||
if (oscillator && gainNode) {
|
||||
oscillator.frequency.setValueAtTime(newFreq, audioContext.currentTime);
|
||||
gainNode.gain.setTargetAtTime(0.5, audioContext.currentTime, 0.01);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseDown = (e) => {
|
||||
if (isReadOnly) return;
|
||||
const target = e.target;
|
||||
const isSlider = target.classList.contains('otamatone-neck');
|
||||
const isIndicator = target.classList.contains('frequency-indicator') ||
|
||||
target.closest('.frequency-indicator');
|
||||
|
||||
setDragging(true);
|
||||
handleFrequencyChange(e);
|
||||
if (!isSlider && !isIndicator) return;
|
||||
|
||||
if (!audioContextRef.current) {
|
||||
audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)();
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!isPlaying) {
|
||||
startAudio();
|
||||
}
|
||||
if (!audioContext) initAudio();
|
||||
isPressed.current = true;
|
||||
isDragging.current = true;
|
||||
|
||||
if (gainNode) {
|
||||
gainNode.gain.setTargetAtTime(0.5, audioContext.currentTime, 0.01);
|
||||
}
|
||||
|
||||
document.body.style.userSelect = 'none';
|
||||
const iframes = document.querySelectorAll('iframe');
|
||||
iframes.forEach(iframe => {
|
||||
iframe.style.pointerEvents = 'none';
|
||||
});
|
||||
|
||||
onFrequencyChange(frequency.current, true);
|
||||
handleMouseMove(e);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setDragging(false);
|
||||
const handleMouseUp = (e) => {
|
||||
if (!isPressed.current) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
isPressed.current = false;
|
||||
isDragging.current = false;
|
||||
|
||||
if (gainNode) {
|
||||
gainNode.gain.setTargetAtTime(0.00001, audioContext.currentTime, 0.05);
|
||||
}
|
||||
|
||||
document.body.style.userSelect = '';
|
||||
const iframes = document.querySelectorAll('iframe');
|
||||
iframes.forEach(iframe => {
|
||||
iframe.style.pointerEvents = 'auto';
|
||||
});
|
||||
|
||||
onFrequencyChange(frequency.current, false);
|
||||
};
|
||||
|
||||
const handleFrequencyChange = (e) => {
|
||||
if (isReadOnly) return;
|
||||
document.addEventListener('mousemove', handleMouseMove, true);
|
||||
document.addEventListener('mouseup', handleMouseUp, true);
|
||||
|
||||
const rect = sliderRef.current.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const clampedX = Math.max(0, Math.min(x, rect.width));
|
||||
const newFrequency = 20 + (clampedX / rect.width) * 1980;
|
||||
setFrequency(newFrequency);
|
||||
const slider = sliderRef.current;
|
||||
if (slider) {
|
||||
slider.addEventListener('mousedown', handleMouseDown, true);
|
||||
}
|
||||
|
||||
if (onFrequencyChange) {
|
||||
onFrequencyChange(newFrequency);
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove, true);
|
||||
document.removeEventListener('mouseup', handleMouseUp, true);
|
||||
if (slider) {
|
||||
slider.removeEventListener('mousedown', handleMouseDown, true);
|
||||
}
|
||||
document.body.style.userSelect = '';
|
||||
const iframes = document.querySelectorAll('iframe');
|
||||
iframes.forEach(iframe => {
|
||||
iframe.style.pointerEvents = 'auto';
|
||||
});
|
||||
};
|
||||
}, [isReadOnly, onFrequencyChange, audioContext, oscillator, gainNode, initAudio]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (e) => dragging && handleFrequencyChange(e);
|
||||
const handleMouseUpGlobal = () => dragging && handleMouseUp();
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUpGlobal);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUpGlobal);
|
||||
};
|
||||
}, [dragging]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPlaying && oscillatorRef.current && audioContextRef.current) {
|
||||
oscillatorRef.current.frequency.setValueAtTime(
|
||||
frequency,
|
||||
audioContextRef.current.currentTime
|
||||
);
|
||||
}
|
||||
}, [frequency, isPlaying]);
|
||||
|
||||
const startAudio = () => {
|
||||
if (!audioContextRef.current) {
|
||||
try {
|
||||
audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)();
|
||||
} catch (e) {
|
||||
console.error("AudioContext could not be created:", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (audioContextRef.current.state === 'suspended') {
|
||||
audioContextRef.current.resume().catch(err => {
|
||||
console.error("Could not resume AudioContext:", err);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (gainNode) gainNode.gain.setValueAtTime(0, audioContext.currentTime);
|
||||
if (oscillator) {
|
||||
try {
|
||||
if (!oscillatorRef.current) {
|
||||
oscillatorRef.current = audioContextRef.current.createOscillator();
|
||||
oscillatorRef.current.type = 'sine';
|
||||
oscillatorRef.current.frequency.setValueAtTime(
|
||||
frequency,
|
||||
audioContextRef.current.currentTime
|
||||
);
|
||||
|
||||
gainNodeRef.current = audioContextRef.current.createGain();
|
||||
gainNodeRef.current.gain.setValueAtTime(0.5, audioContextRef.current.currentTime);
|
||||
|
||||
oscillatorRef.current.connect(gainNodeRef.current);
|
||||
gainNodeRef.current.connect(audioContextRef.current.destination);
|
||||
oscillatorRef.current.start();
|
||||
console.log("Audio started successfully");
|
||||
} else {
|
||||
oscillatorRef.current.frequency.setValueAtTime(
|
||||
frequency,
|
||||
audioContextRef.current.currentTime
|
||||
);
|
||||
}
|
||||
setIsPlaying(true);
|
||||
oscillator.stop();
|
||||
} catch (e) {
|
||||
console.error("Error starting audio:", e);
|
||||
console.warn("Oscillator already stopped");
|
||||
}
|
||||
}
|
||||
if (audioContext) audioContext.close();
|
||||
};
|
||||
}, [oscillator, audioContext, gainNode]);
|
||||
|
||||
const stopAudio = () => {
|
||||
if (oscillatorRef.current) {
|
||||
try {
|
||||
oscillatorRef.current.stop();
|
||||
oscillatorRef.current.disconnect();
|
||||
oscillatorRef.current = null;
|
||||
setIsPlaying(false);
|
||||
console.log("Audio stopped");
|
||||
} catch (e) {
|
||||
console.error("Error stopping audio:", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
const getFrequencyPosition = () => {
|
||||
const pos = ((frequency.current - 220) / 880) * 100;
|
||||
return `${Math.max(0, Math.min(pos, 100))}%`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (oscillatorRef.current) {
|
||||
try {
|
||||
oscillatorRef.current.stop();
|
||||
oscillatorRef.current.disconnect();
|
||||
} catch (e) {
|
||||
console.error("Error cleaning up oscillator:", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (audioContextRef.current && audioContextRef.current.state !== 'closed') {
|
||||
try {
|
||||
audioContextRef.current.close();
|
||||
} catch (e) {
|
||||
console.error("Error closing AudioContext:", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (isPlaying) {
|
||||
stopAudio();
|
||||
}
|
||||
};
|
||||
}, [isPlaying]);
|
||||
|
||||
return (
|
||||
<div className={`otamatone-container ${isReadOnly ? 'read-only' : ''}`}>
|
||||
<div
|
||||
className="otamatone"
|
||||
onMouseDown={handleMouseDown}
|
||||
style={{ cursor: isReadOnly ? 'default' : 'pointer' }}
|
||||
>
|
||||
<div className="otamatone-face">
|
||||
<div className="otamatone-mouth" style={{
|
||||
height: `${10 + (frequency / 2000) * 40}px`,
|
||||
width: `${10 + (frequency / 2000) * 40}px`
|
||||
}}></div>
|
||||
<div className="otamatone-eyes">
|
||||
<div className="eye left-eye"></div>
|
||||
<div className="eye right-eye"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="otamatone-neck" ref={sliderRef}>
|
||||
<div className="frequency-indicator" style={{left: `${(frequency - 20) / 1980 * 100}%`}}></div>
|
||||
<div className="note-marker" style={{left: '10%', pointerEvents: 'none'}}>♩</div>
|
||||
<div className="note-marker" style={{left: '50%', pointerEvents: 'none'}}>♪</div>
|
||||
<div className="note-marker" style={{left: '90%', pointerEvents: 'none'}}>♫</div>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div className="otamatone-container">
|
||||
<div className="otamatone">
|
||||
<div className="otamatone-face">
|
||||
<div className="otamatone-eyes">
|
||||
<div className="eye left-eye"></div>
|
||||
<div className="eye right-eye"></div>
|
||||
</div>
|
||||
<div className="otamatone-mouth"></div>
|
||||
</div>
|
||||
);
|
||||
<div
|
||||
ref={sliderRef}
|
||||
className={`otamatone-neck ${!isReadOnly ? 'interactive' : ''}`}
|
||||
style={{ cursor: isReadOnly ? 'default' : 'pointer' }}
|
||||
>
|
||||
<div
|
||||
className={`frequency-indicator ${isReadOnly ? 'read-only' : ''} ${composerIsPlaying ? 'active' : ''}`}
|
||||
style={{ left: getFrequencyPosition() }}
|
||||
>
|
||||
<div className="note-marker">
|
||||
{(!isReadOnly && isDragging.current) && Math.round(frequency.current)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -63,6 +63,10 @@
|
||||
background: rgba(255, 255, 255, 0.2)
|
||||
pointer-events: none
|
||||
|
||||
&.interactive
|
||||
.frequency-indicator:hover
|
||||
transform: translateX(-50%) scale(1.1)
|
||||
|
||||
.frequency-indicator
|
||||
position: absolute
|
||||
top: -20px
|
||||
@ -84,6 +88,19 @@
|
||||
cursor: grabbing
|
||||
transform: translateX(-50%) scale(0.95)
|
||||
|
||||
&.read-only
|
||||
pointer-events: none
|
||||
cursor: default
|
||||
opacity: 0.8
|
||||
|
||||
box-shadow: 0 0 15px rgba(255, 107, 179, 0.4)
|
||||
|
||||
&.active
|
||||
opacity: 1
|
||||
box-shadow: 0 0 30px rgba(255, 107, 179, 0.8)
|
||||
transform: translateX(-50%) scale(1.1)
|
||||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)
|
||||
|
||||
.note-marker
|
||||
position: absolute
|
||||
top: -50px
|
||||
|
@ -780,6 +780,10 @@
|
||||
width: 100%
|
||||
height: auto
|
||||
aspect-ratio: 16 / 9
|
||||
transition: pointer-events 0.1s ease
|
||||
|
||||
&:hover .song-embedded-player
|
||||
pointer-events: auto !important
|
||||
|
||||
.results-phase
|
||||
display: flex
|
||||
|
@ -221,7 +221,7 @@ module.exports = (io) => (socket) => {
|
||||
if (currentRoomId) socket.emit("room-code", currentRoomId);
|
||||
});
|
||||
|
||||
socket.on("submit-frequency", ({ frequency }) => {
|
||||
socket.on("submit-frequency", ({ frequency, isPlaying }) => {
|
||||
const roomId = roomController.getUserRoom(socket.id);
|
||||
if (!roomId) return;
|
||||
|
||||
@ -229,7 +229,7 @@ module.exports = (io) => (socket) => {
|
||||
if (userRole !== 'composer') return;
|
||||
|
||||
if (gameController.updateFrequency(roomId, frequency)) {
|
||||
socket.to(roomId).emit("frequency-update", { frequency });
|
||||
socket.to(roomId).emit("frequency-update", { frequency, isPlaying });
|
||||
}
|
||||
});
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user