Migrate to components

This commit is contained in:
2025-02-28 18:25:12 +01:00
parent 7a581749a9
commit 7d5813b1c2
10 changed files with 309 additions and 311 deletions

View File

@ -0,0 +1,108 @@
import {useEffect, useRef, useState} from "react";
import "./styles.sass";
export const MusicSlider = () => {
const [frequency, setFrequency] = useState(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 handleMouseDown = (e) => {
setDragging(true);
startAudio();
handleFrequencyChange(e);
};
const handleMouseUp = () => {
setDragging(false);
stopAudio();
};
const handleFrequencyChange = (e) => {
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);
};
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) {
if (oscillatorRef.current) {
oscillatorRef.current.frequency.setValueAtTime(frequency, audioContextRef.current.currentTime);
}
}
}, [frequency, isPlaying]);
const startAudio = () => {
if (!audioContextRef.current) {
audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)();
}
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();
} else {
oscillatorRef.current.frequency.setValueAtTime(frequency, audioContextRef.current.currentTime);
}
setIsPlaying(true);
};
const stopAudio = () => {
if (oscillatorRef.current) {
oscillatorRef.current.stop();
oscillatorRef.current.disconnect();
oscillatorRef.current = null;
}
setIsPlaying(false);
};
return (
<div className="otamatone-container">
<div className="otamatone" onMouseDown={handleMouseDown}>
<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 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>
</div>
)
}

View File

@ -0,0 +1 @@
export {MusicSlider as default} from "./MusicSlider.jsx";

View File

@ -0,0 +1,82 @@
.otamatone-container
display: flex
justify-content: center
align-items: center
position: fixed
left: 0
right: 0
bottom: 0
padding: 20px
background-color: rgba(30, 30, 30, 0.5)
backdrop-filter: blur(10px)
border-radius: 20px
margin: 20px
z-index: 1
.otamatone
display: flex
flex-direction: row
align-items: center
cursor: pointer
padding: 10px
width: 100%
.otamatone-neck
flex: 1
height: 20px
background: linear-gradient(135deg, #000, #444)
border-radius: 10px
position: relative
.frequency-indicator
position: absolute
top: -10px
width: 20px
height: 20px
background-color: #ff0000
border-radius: 50%
transform: translateX(-50%)
.note-marker
position: absolute
top: -30px
font-size: 24pt
color: #fff
.otamatone-face
width: 100px
height: 100px
background: radial-gradient(circle, #fff, #ddd)
border-radius: 50%
display: flex
flex-direction: column
align-items: center
justify-content: center
position: absolute
left: 0
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2)
.otamatone-eyes
display: flex
justify-content: space-between
width: 60%
position: absolute
top: 30px
.eye
width: 15px
height: 15px
background-color: #000
border-radius: 50%
.left-eye
margin-right: 10px
.right-eye
margin-left: 10px
.otamatone-mouth
background-color: #000
border-radius: 50%
position: absolute
bottom: 10px