diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fd6ff18..2a05fd8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,6 +4,7 @@ import BackgroundScene from './components/BackgroundScene'; import MusicPlayer from './components/MusicPlayer'; import Timer from './components/Timer'; import Notes from './components/Notes'; +import WhiteNoise from './components/WhiteNoise'; import KiraAvatar from './components/KiraAvatar'; import ChatBubble from './components/ChatBubble'; import PetZone from './components/PetZone'; @@ -136,11 +137,12 @@ export default function App() { /> - {/* Column 2: Timer + Music */} + {/* Column 2: Timer + Music + Notes + WhiteNoise */}
+
{/* Column 3: Chat + Text Input */} diff --git a/frontend/src/components/WhiteNoise.tsx b/frontend/src/components/WhiteNoise.tsx new file mode 100644 index 0000000..947ee48 --- /dev/null +++ b/frontend/src/components/WhiteNoise.tsx @@ -0,0 +1,147 @@ +import { useState, useRef, useCallback } from 'react'; + +type NoiseType = 'white' | 'pink' | 'brown' | 'rain' | 'cafe'; + +const NOISE_PRESETS: { id: NoiseType; label: string; icon: string }[] = [ + { id: 'white', label: 'White', icon: '⚪' }, + { id: 'pink', label: 'Pink', icon: '🌸' }, + { id: 'brown', label: 'Brown', icon: '🤎' }, + { id: 'rain', label: 'Rain', icon: '🌧️' }, + { id: 'cafe', label: 'Cafe', icon: '☕' }, +]; + +export default function WhiteNoise() { + const [active, setActive] = useState(null); + const [volume, setVolume] = useState(0.25); + const audioCtxRef = useRef(null); + const noiseSourceRef = useRef(null); + const gainRef = useRef(null); + const filterRef = useRef(null); + + const stopNoise = useCallback(() => { + if (noiseSourceRef.current) { + try { noiseSourceRef.current.stop(); } catch {} + noiseSourceRef.current = null; + } + if (audioCtxRef.current) { + // keep context for reuse + } + setActive(null); + }, []); + + const startNoise = useCallback((type: NoiseType) => { + stopNoise(); + + const ctx = audioCtxRef.current || new (window.AudioContext || (window as any).webkitAudioContext)(); + audioCtxRef.current = ctx; + + const bufferSize = 2 * ctx.sampleRate; + const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); + const data = buffer.getChannelData(0); + + // Generate noise + let lastOut = 0; + for (let i = 0; i < bufferSize; i++) { + let white = Math.random() * 2 - 1; + if (type === 'pink') { + // Pink noise approx + data[i] = (lastOut + (0.02 * white)) / 1.02; + lastOut = data[i]; + data[i] *= 3.5; // gain + } else if (type === 'brown') { + // Brown/red noise + data[i] = (lastOut + (0.02 * white)) / 1.02; + lastOut = data[i]; + data[i] *= 3.5; + } else { + data[i] = white; + } + } + + const source = ctx.createBufferSource(); + source.buffer = buffer; + source.loop = true; + + const gain = ctx.createGain(); + gain.gain.value = volume; + + let node: AudioNode = source; + + if (type === 'rain' || type === 'cafe') { + const filter = ctx.createBiquadFilter(); + filter.type = type === 'rain' ? 'lowpass' : 'bandpass'; + filter.frequency.value = type === 'rain' ? 800 : 1200; + filter.Q.value = type === 'rain' ? 0.7 : 1.5; + node.connect(filter); + node = filter; + filterRef.current = filter; + } + + node.connect(gain); + gain.connect(ctx.destination); + + source.start(); + noiseSourceRef.current = source; + gainRef.current = gain; + + setActive(type); + }, [volume, stopNoise]); + + const toggle = (type: NoiseType) => { + if (active === type) { + stopNoise(); + } else { + startNoise(type); + } + }; + + const changeVolume = (v: number) => { + setVolume(v); + if (gainRef.current) { + gainRef.current.gain.value = v; + } + }; + + return ( +
+

+ 🌬️ White Noise +

+ +
+ {NOISE_PRESETS.map((p) => ( + + ))} +
+ +
+ Vol + changeVolume(parseFloat(e.target.value))} + className="flex-1 accent-kira-pink" + /> + {Math.round(volume * 100)}% +
+ + {active && ( +
Playing {active} noise — body double approved ✨
+ )} +
+ ); +} diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index 9f8e41c..13fd1c6 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AnimatedAvatar.tsx","./src/components/BackgroundScene.tsx","./src/components/ChatBubble.tsx","./src/components/Clock.tsx","./src/components/KiraAvatar.tsx","./src/components/MusicPlayer.tsx","./src/components/Notes.tsx","./src/components/Particles.tsx","./src/components/PetZone.tsx","./src/components/Timer.tsx","./src/components/Toolbar.tsx","./src/components/Wardrobe.tsx","./src/components/WelcomeScreen.tsx","./src/components/scenes.ts","./src/hooks/useConversation.ts","./src/types/index.ts"],"version":"6.0.3"} \ No newline at end of file +{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AnimatedAvatar.tsx","./src/components/BackgroundScene.tsx","./src/components/ChatBubble.tsx","./src/components/Clock.tsx","./src/components/KiraAvatar.tsx","./src/components/MusicPlayer.tsx","./src/components/Notes.tsx","./src/components/Particles.tsx","./src/components/PetZone.tsx","./src/components/Timer.tsx","./src/components/Toolbar.tsx","./src/components/Wardrobe.tsx","./src/components/WelcomeScreen.tsx","./src/components/WhiteNoise.tsx","./src/components/scenes.ts","./src/hooks/useConversation.ts","./src/types/index.ts"],"version":"6.0.3"} \ No newline at end of file