Skip to content

Commit e259a34

Browse files
committed
feat: mouse down for cozy place. [p.cv style]
1 parent f97ece7 commit e259a34

File tree

1 file changed

+142
-93
lines changed

1 file changed

+142
-93
lines changed

src/pages/apps/CozyAppPage.js

Lines changed: 142 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)