@@ -32,6 +32,79 @@ const CozyAppPage = () => {
3232 const gainNodeRef = useRef ( null ) ;
3333 const sourceNodeRef = useRef ( null ) ;
3434 const canvasRef = useRef ( null ) ;
35+ const isMouseDownRef = useRef ( false ) ; // Ref to hold the current state of isMouseDown
36+ const particlesRef = useRef ( [ ] ) ; // New: persistent storage for particles
37+
38+ class Particle { // Move Particle class definition here
39+ constructor ( mode , canvas ) {
40+ this . mode = mode ;
41+ this . canvas = canvas ;
42+ this . ctx = canvas . getContext ( '2d' ) ;
43+ this . reset ( ) ;
44+ }
45+
46+ reset ( ) {
47+ if ( this . mode === 'fireplace' ) {
48+ const spread = this . canvas . width * 0.5 ;
49+ this . x = this . canvas . width / 2 + ( Math . random ( ) - 0.5 ) * spread ;
50+ this . y = this . canvas . height ;
51+ this . size = Math . random ( ) * 10 + 5 ;
52+ this . speedY = Math . random ( ) * 5 + 2 ;
53+ this . speedX = ( Math . random ( ) - 0.5 ) * 2 ;
54+ this . color = `hsla(${ Math . random ( ) * 40 + 10 } , 100%, 50%, ${ Math . random ( ) * 0.5 + 0.1 } )` ;
55+ this . life = 150 ;
56+ this . decay = Math . random ( ) * 0.5 + 0.2 ;
57+ } else if ( this . mode === 'snow' ) {
58+ this . x = Math . random ( ) * this . canvas . width ;
59+ this . y = Math . random ( ) * this . canvas . height * - 1 ; // Start above canvas
60+ this . size = Math . random ( ) * 3 + 1 ;
61+ this . speedY = Math . random ( ) * 1 + 0.5 ;
62+ this . speedX = ( Math . random ( ) - 0.5 ) * 0.5 ; // Gentle drift
63+ this . color = `hsla(0, 0%, 100%, ${ Math . random ( ) * 0.5 + 0.3 } )` ;
64+ } else if ( this . mode === 'rain' ) {
65+ this . x = Math . random ( ) * this . canvas . width ;
66+ this . y = Math . random ( ) * this . canvas . height * - 1 ;
67+ this . size = Math . random ( ) * 20 + 10 ; // Length of rain drop
68+ this . speedY = Math . random ( ) * 10 + 15 ; // Fast
69+ this . speedX = 0 ;
70+ this . color = `hsla(210, 100%, 70%, ${ Math . random ( ) * 0.3 + 0.1 } )` ;
71+ }
72+ }
73+
74+ update ( ) {
75+ this . x += this . speedX ;
76+ this . y += ( this . mode === 'fireplace' ? - 1 : 1 ) * this . speedY ; // Fire goes up, rain/snow goes down
77+
78+ if ( this . mode === 'fireplace' ) {
79+ this . size -= 0.1 ;
80+ this . life -= this . decay ;
81+ if ( this . size <= 0 || this . life <= 0 ) this . reset ( ) ;
82+ } else {
83+ // Wrap around for snow/rain
84+ if ( this . y > this . canvas . height ) {
85+ this . y = - 10 ;
86+ this . x = Math . random ( ) * this . canvas . width ;
87+ }
88+ if ( this . x > this . canvas . width ) this . x = 0 ;
89+ if ( this . x < 0 ) this . x = this . canvas . width ;
90+ }
91+ }
92+
93+ draw ( ) {
94+ this . ctx . beginPath ( ) ;
95+ if ( this . mode === 'rain' ) {
96+ this . ctx . moveTo ( this . x , this . y ) ;
97+ this . ctx . lineTo ( this . x , this . y + this . size ) ;
98+ this . ctx . strokeStyle = this . color ;
99+ this . ctx . lineWidth = 1 ;
100+ this . ctx . stroke ( ) ;
101+ } else {
102+ this . ctx . arc ( this . x , this . y , this . size , 0 , Math . PI * 2 ) ;
103+ this . ctx . fillStyle = this . color ;
104+ this . ctx . fill ( ) ;
105+ }
106+ }
107+ }
35108
36109 // --- Audio Engine ---
37110 useEffect ( ( ) => {
@@ -49,24 +122,20 @@ const CozyAppPage = () => {
49122 audioCtxRef . current = null ;
50123 }
51124 } ;
52-
53125 if ( isMuted || mode === 'breathe' ) {
54126 cleanupAudio ( ) ;
55127 return ;
56128 }
57-
58129 // Initialize Audio Context
59130 const AudioContext = window . AudioContext || window . webkitAudioContext ;
60131 if ( ! audioCtxRef . current ) {
61132 audioCtxRef . current = new AudioContext ( ) ;
62133 }
63134 const ctx = audioCtxRef . current ;
64-
65135 // Create Master Gain (Volume)
66136 gainNodeRef . current = ctx . createGain ( ) ;
67137 gainNodeRef . current . connect ( ctx . destination ) ;
68138 gainNodeRef . current . gain . value = 0.15 ; // Master volume
69-
70139 // Generate Noise Buffer (Pink-ish Noise for nature sounds)
71140 const bufferSize = 2 * ctx . sampleRate ;
72141 const noiseBuffer = ctx . createBuffer ( 1 , bufferSize , ctx . sampleRate ) ;
@@ -78,17 +147,14 @@ const CozyAppPage = () => {
78147 lastOut = output [ i ] ;
79148 output [ i ] *= 3.5 ;
80149 }
81-
82150 // Source setup
83151 sourceNodeRef . current = ctx . createBufferSource ( ) ;
84152 sourceNodeRef . current . buffer = noiseBuffer ;
85153 sourceNodeRef . current . loop = true ;
86-
87154 // Filter setup based on mode
88155 const filter = ctx . createBiquadFilter ( ) ;
89156 sourceNodeRef . current . connect ( filter ) ;
90157 filter . connect ( gainNodeRef . current ) ;
91-
92158 if ( mode === 'fireplace' ) {
93159 filter . type = 'lowpass' ;
94160 filter . frequency . value = 400 ;
@@ -103,14 +169,16 @@ const CozyAppPage = () => {
103169 filter . frequency . value = 200 ; // Deep, soft wind rumble
104170 gainNodeRef . current . gain . value = 0.05 ; // Very quiet and soothing
105171 }
106-
107172 sourceNodeRef . current . start ( ) ;
108-
109173 return cleanupAudio ;
110174 } , [ mode , isMuted ] ) ;
111175
176+ // --- Particle Engine ---
112177 useEffect ( ( ) => {
113- if ( mode === 'breathe' ) return ;
178+ if ( mode === 'breathe' ) {
179+ particlesRef . current = [ ] ; // Clear particles if switching to breathe mode
180+ return ;
181+ }
114182
115183 const canvas = canvasRef . current ;
116184 if ( ! canvas ) return ;
@@ -125,88 +193,38 @@ const CozyAppPage = () => {
125193 resizeCanvas ( ) ;
126194 window . addEventListener ( 'resize' , resizeCanvas ) ;
127195
128- let particles = [ ] ;
129- let particleCount = 100 ;
196+ let baseParticleCount = 100 ;
130197
131198 // Config based on mode
132- if ( mode === 'fireplace' ) particleCount = 300 ;
133- if ( mode === 'snow' ) particleCount = 200 ;
134- if ( mode === 'rain' ) particleCount = 500 ;
135-
136- class Particle {
137- constructor ( ) {
138- this . reset ( ) ;
139- }
140-
141- reset ( ) {
142- if ( mode === 'fireplace' ) {
143- const spread = canvas . width * 0.5 ;
144- this . x = canvas . width / 2 + ( Math . random ( ) - 0.5 ) * spread ;
145- this . y = canvas . height ;
146- this . size = Math . random ( ) * 10 + 5 ;
147- this . speedY = Math . random ( ) * 5 + 2 ;
148- this . speedX = ( Math . random ( ) - 0.5 ) * 2 ;
149- this . color = `hsla(${ Math . random ( ) * 40 + 10 } , 100%, 50%, ${ Math . random ( ) * 0.5 + 0.1 } )` ;
150- this . life = 150 ;
151- this . decay = Math . random ( ) * 0.5 + 0.2 ;
152- } else if ( mode === 'snow' ) {
153- this . x = Math . random ( ) * canvas . width ;
154- this . y = Math . random ( ) * canvas . height * - 1 ; // Start above canvas
155- this . size = Math . random ( ) * 3 + 1 ;
156- this . speedY = Math . random ( ) * 1 + 0.5 ;
157- this . speedX = ( Math . random ( ) - 0.5 ) * 0.5 ; // Gentle drift
158- this . color = `hsla(0, 0%, 100%, ${ Math . random ( ) * 0.5 + 0.3 } )` ;
159- } else if ( mode === 'rain' ) {
160- this . x = Math . random ( ) * canvas . width ;
161- this . y = Math . random ( ) * canvas . height * - 1 ;
162- this . size = Math . random ( ) * 20 + 10 ; // Length of rain drop
163- this . speedY = Math . random ( ) * 10 + 15 ; // Fast
164- this . speedX = 0 ;
165- this . color = `hsla(210, 100%, 70%, ${ Math . random ( ) * 0.3 + 0.1 } )` ;
166- }
167- }
168-
169- update ( ) {
170- this . x += this . speedX ;
171- this . y += ( mode === 'fireplace' ? - 1 : 1 ) * this . speedY ; // Fire goes up, rain/snow goes down
199+ if ( mode === 'fireplace' ) baseParticleCount = 300 ;
200+ if ( mode === 'snow' ) baseParticleCount = 200 ;
201+ if ( mode === 'rain' ) baseParticleCount = 500 ;
202+
203+ // When mode changes, reset existing particles to adapt to the new mode
204+ particlesRef . current . forEach ( p => {
205+ p . mode = mode ; // Update mode reference
206+ p . canvas = canvas ; // Update canvas reference
207+ p . ctx = ctx ; // Update ctx reference
208+ p . reset ( ) ;
209+ } ) ;
172210
173- if ( mode === 'fireplace' ) {
174- this . size -= 0.1 ;
175- this . life -= this . decay ;
176- if ( this . size <= 0 || this . life <= 0 ) this . reset ( ) ;
177- } else {
178- // Wrap around for snow/rain
179- if ( this . y > canvas . height ) {
180- this . y = - 10 ;
181- this . x = Math . random ( ) * canvas . width ;
182- }
183- if ( this . x > canvas . width ) this . x = 0 ;
184- if ( this . x < 0 ) this . x = canvas . width ;
211+ const animate = ( ) => {
212+ // Calculate targetParticleCount inside animate, to react to isMouseDown changes
213+ const targetParticleCount = isMouseDownRef . current ? baseParticleCount * 5 : baseParticleCount ;
214+
215+ // Add or remove particles dynamically per frame
216+ if ( particlesRef . current . length < targetParticleCount ) {
217+ // Add a few particles per frame to smoothly increase
218+ for ( let i = 0 ; i < Math . min ( 15 , targetParticleCount - particlesRef . current . length ) ; i ++ ) {
219+ particlesRef . current . push ( new Particle ( mode , canvas ) ) ;
185220 }
186- }
187-
188- draw ( ) {
189- ctx . beginPath ( ) ;
190- if ( mode === 'rain' ) {
191- ctx . moveTo ( this . x , this . y ) ;
192- ctx . lineTo ( this . x , this . y + this . size ) ;
193- ctx . strokeStyle = this . color ;
194- ctx . lineWidth = 1 ;
195- ctx . stroke ( ) ;
196- } else {
197- ctx . arc ( this . x , this . y , this . size , 0 , Math . PI * 2 ) ;
198- ctx . fillStyle = this . color ;
199- ctx . fill ( ) ;
221+ } else if ( particlesRef . current . length > targetParticleCount ) {
222+ // Remove a few particles per frame to smoothly decrease
223+ for ( let i = 0 ; i < Math . min ( 15 , particlesRef . current . length - targetParticleCount ) ; i ++ ) {
224+ particlesRef . current . pop ( ) ;
200225 }
201226 }
202- }
203227
204- // Initialize particles
205- for ( let i = 0 ; i < particleCount ; i ++ ) {
206- particles . push ( new Particle ( ) ) ;
207- }
208-
209- const animate = ( ) => {
210228 ctx . clearRect ( 0 , 0 , canvas . width , canvas . height ) ;
211229
212230 if ( mode === 'fireplace' ) {
@@ -223,7 +241,7 @@ const CozyAppPage = () => {
223241 ctx . globalCompositeOperation = 'source-over' ;
224242 }
225243
226- particles . forEach ( ( p ) => {
244+ particlesRef . current . forEach ( ( p ) => {
227245 p . update ( ) ;
228246 p . draw ( ) ;
229247 } ) ;
@@ -234,13 +252,30 @@ const CozyAppPage = () => {
234252 animationFrameId = requestAnimationFrame ( animate ) ;
235253 } ;
236254
255+ // Initial fill if particlesRef.current is empty when mode starts
256+ if ( particlesRef . current . length === 0 ) {
257+ for ( let i = 0 ; i < baseParticleCount ; i ++ ) {
258+ particlesRef . current . push ( new Particle ( mode , canvas ) ) ;
259+ }
260+ }
261+
237262 animate ( ) ;
238263
239264 return ( ) => {
240265 window . removeEventListener ( 'resize' , resizeCanvas ) ;
241266 cancelAnimationFrame ( animationFrameId ) ;
242267 } ;
243- } , [ mode ] ) ;
268+ // eslint-disable-next-line react-hooks/exhaustive-deps
269+ } , [ mode ] ) ; // isMouseDown removed from dependencies
270+
271+ const handleMouseDownIntensify = ( ) => {
272+ if ( mode === 'breathe' ) return ;
273+ isMouseDownRef . current = true ; // Update ref directly
274+ } ;
275+
276+ const handleMouseUpIntensify = ( ) => {
277+ isMouseDownRef . current = false ; // Update ref directly
278+ } ;
244279
245280 return (
246281 < div className = "min-h-screen bg-gray-900 text-gray-100 flex flex-col transition-colors duration-500" >
@@ -315,18 +350,32 @@ const CozyAppPage = () => {
315350
316351 { /* Content Area */ }
317352 { /*<div className="w-[830px] h-[800px] relative bg-gray-800 rounded-2xl overflow-hidden shadow-2xl border border-gray-700">*/ }
318- < div className = "flex-grow h-[80vh] relative bg-gray-800 rounded-2xl overflow-hidden shadow-2xl border border-gray-700" >
353+ < div
354+ className = "flex-grow h-[80vh] relative bg-gray-800 rounded-2xl overflow-hidden shadow-2xl border border-gray-700"
355+ onMouseDown = { handleMouseDownIntensify } // Use onMouseDown
356+ onMouseUp = { handleMouseUpIntensify } // Use onMouseUp
357+ onMouseLeave = { handleMouseUpIntensify } // Reset if mouse leaves while down
358+ >
319359 { mode !== 'breathe' && (
320360 < div className = "absolute inset-0 flex flex-col items-center justify-end pb-10" >
321361 < canvas
322362 ref = { canvasRef }
323363 className = "absolute inset-0 w-full h-full"
324364 />
325- < div className = "relative z-10 text-amber-200/50 font-mono text-sm select-none pointer-events-none mb-4" >
326- { mode === 'fireplace' && 'Warmth & comfort' }
327- { mode === 'snow' && 'Silent snow' }
328- { mode === 'rain' && 'Gentle rain' }
329- </ div >
365+ { mode !== 'breathe' && (
366+ < >
367+ { ! isMouseDownRef . current && (
368+ < div className = "relative z-10 text-gray-300/35 font-mono text-xs select-none pointer-events-none mb-2" >
369+ ...hold...
370+ </ div >
371+ ) }
372+ < div className = "relative z-10 text-amber-200/50 font-mono text-sm select-none pointer-events-none mb-4" >
373+ { mode === 'fireplace' && 'Warmth & comfort' }
374+ { mode === 'snow' && 'Silent snow' }
375+ { mode === 'rain' && 'Gentle rain' }
376+ </ div >
377+ </ >
378+ ) }
330379 </ div >
331380 ) }
332381
0 commit comments