raayraay99 commited on
Commit
24883ad
·
1 Parent(s): 7f8fcea

✨ Viral upgrade: audio-reactive, fullscreen, video export

Browse files
Files changed (2) hide show
  1. README.md +26 -6
  2. index.html +522 -341
README.md CHANGED
@@ -1,12 +1,32 @@
1
  ---
2
- title: hypnotic-flocking
3
  emoji: 🐳
4
  colorFrom: blue
5
- colorTo: blue
6
  sdk: static
7
- pinned: false
8
- tags:
9
- - deepsite
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Watch Whales Think 🐳
3
  emoji: 🐳
4
  colorFrom: blue
5
+ colorTo: green
6
  sdk: static
7
+ pinned: true
8
+ short_description: Mesmerizing boids simulation with audio reactivity
 
9
  ---
10
 
11
+ # 🐳 Hypnotic Flocking
12
+
13
+ **Watch whales think** — A mesmerizing simulation of emergent flocking behavior.
14
+
15
+ ## Features
16
+
17
+ - 🎨 **Fullscreen Canvas** — Immersive visual experience
18
+ - 🎤 **Audio Reactive** — Whales dance to your music (press SPACE)
19
+ - 📹 **MP4 Export** — Record 30s clips for TikTok (press R)
20
+ - ⚙️ **Minimal Controls** — Chaos, Count, Trail Glow
21
+
22
+ ## Keyboard Shortcuts
23
+
24
+ | Key | Action |
25
+ |-----|--------|
26
+ | `F` | Toggle controls |
27
+ | `SPACE` | Toggle audio reactive |
28
+ | `R` | Start/stop recording |
29
+
30
+ ---
31
+
32
+ Made with emergent algorithms and neon dreams.
index.html CHANGED
@@ -3,396 +3,577 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Hypnotic Flocking Simulation</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <style>
 
9
  body {
10
- margin: 0;
11
  overflow: hidden;
12
  background: #000;
 
13
  }
14
- canvas {
15
- display: block;
16
- }
17
  .controls {
18
- position: absolute;
19
  top: 20px;
20
  left: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  z-index: 100;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  color: white;
23
- background: rgba(0, 0, 0, 0.7);
24
- padding: 15px;
25
- border-radius: 10px;
26
- font-family: 'Arial', sans-serif;
 
27
  }
28
- .particle {
29
- position: absolute;
 
 
 
 
30
  border-radius: 50%;
31
- pointer-events: none;
 
 
 
 
 
 
32
  }
33
  </style>
34
  </head>
35
  <body>
36
- <canvas id="flockCanvas"></canvas>
37
- <div class="controls">
38
- <h1 class="text-2xl font-bold mb-4 text-purple-400">Hypnotic Flocking</h1>
39
- <div class="grid grid-cols-2 gap-4">
40
- <div>
41
- <label class="block text-sm font-medium text-cyan-300">Alignment</label>
42
- <input type="range" id="alignment" min="0" max="2" step="0.01" value="1" class="w-full">
43
- </div>
44
- <div>
45
- <label class="block text-sm font-medium text-cyan-300">Cohesion</label>
46
- <input type="range" id="cohesion" min="0" max="2" step="0.01" value="1" class="w-full">
47
- </div>
48
- <div>
49
- <label class="block text-sm font-medium text-cyan-300">Separation</label>
50
- <input type="range" id="separation" min="0" max="2" step="0.01" value="1.5" class="w-full">
51
- </div>
52
- <div>
53
- <label class="block text-sm font-medium text-cyan-300">Particle Count</label>
54
- <input type="range" id="particleCount" min="50" max="1000" step="10" value="500" class="w-full">
55
- </div>
56
- <div>
57
- <label class="block text-sm font-medium text-cyan-300">Trail Length</label>
58
- <input type="range" id="trailLength" min="0" max="1" step="0.01" value="0.05" class="w-full">
59
- </div>
60
- <div>
61
- <label class="block text-sm font-medium text-cyan-300">Color Mode</label>
62
- <select id="colorMode" class="w-full bg-gray-800 text-white rounded p-1">
63
- <option value="velocity">Velocity</option>
64
- <option value="distance">Distance</option>
65
- <option value="rainbow">Rainbow</option>
66
- <option value="pulse">Pulse</option>
67
- </select>
68
- </div>
69
  </div>
70
- <button id="resetBtn" class="mt-4 px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded text-white">Reset Simulation</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  </div>
72
 
73
  <script>
74
- document.addEventListener('DOMContentLoaded', () => {
75
- const canvas = document.getElementById('flockCanvas');
76
- const ctx = canvas.getContext('2d');
77
-
78
- // Set canvas to full window size
79
- canvas.width = window.innerWidth;
80
- canvas.height = window.innerHeight;
81
-
82
- // Flocking parameters
83
- let params = {
84
- alignment: 1,
85
- cohesion: 1,
86
- separation: 1.5,
87
- particleCount: 500,
88
- trailLength: 0.05,
89
- colorMode: 'velocity',
90
- mouseInfluence: true,
91
- mouseRadius: 100,
92
- mouseStrength: 5
93
- };
94
-
95
- // Initialize controls
96
- document.getElementById('alignment').addEventListener('input', (e) => {
97
- params.alignment = parseFloat(e.target.value);
98
- });
99
-
100
- document.getElementById('cohesion').addEventListener('input', (e) => {
101
- params.cohesion = parseFloat(e.target.value);
102
- });
103
-
104
- document.getElementById('separation').addEventListener('input', (e) => {
105
- params.separation = parseFloat(e.target.value);
106
- });
107
-
108
- document.getElementById('particleCount').addEventListener('input', (e) => {
109
- params.particleCount = parseInt(e.target.value);
110
- resetSimulation();
111
- });
112
-
113
- document.getElementById('trailLength').addEventListener('input', (e) => {
114
- params.trailLength = parseFloat(e.target.value);
115
- });
116
-
117
- document.getElementById('colorMode').addEventListener('change', (e) => {
118
- params.colorMode = e.target.value;
119
- });
120
-
121
- document.getElementById('resetBtn').addEventListener('click', () => {
122
- resetSimulation();
123
- });
124
-
125
- // Mouse position
126
- let mouseX = null;
127
- let mouseY = null;
128
-
129
- canvas.addEventListener('mousemove', (e) => {
130
- mouseX = e.clientX;
131
- mouseY = e.clientY;
132
- });
133
-
134
- canvas.addEventListener('mouseout', () => {
135
- mouseX = null;
136
- mouseY = null;
137
- });
138
 
139
- // Particle class
140
- class Particle {
141
- constructor() {
142
- this.reset();
143
- this.history = [];
144
- this.maxHistory = 5;
145
- }
146
 
147
- reset() {
148
- this.x = Math.random() * canvas.width;
149
- this.y = Math.random() * canvas.height;
150
- this.vx = (Math.random() - 0.5) * 2;
151
- this.vy = (Math.random() - 0.5) * 2;
152
- this.size = 2 + Math.random() * 3;
153
- this.color = `hsl(${Math.random() * 360}, 100%, 50%)`;
154
- this.baseHue = Math.random() * 360;
155
- this.pulsePhase = Math.random() * Math.PI * 2;
156
- }
157
 
158
- update(particles) {
159
- // Store position history for trails
160
- this.history.unshift({x: this.x, y: this.y});
161
- if (this.history.length > this.maxHistory) {
162
- this.history.pop();
163
- }
164
 
165
- // Apply flocking rules
166
- this.flock(particles);
167
-
168
- // Apply mouse influence
169
- if (mouseX !== null && mouseY !== null && params.mouseInfluence) {
170
- const dx = mouseX - this.x;
171
- const dy = mouseY - this.y;
172
- const distance = Math.sqrt(dx * dx + dy * dy);
173
 
174
- if (distance < params.mouseRadius) {
175
- const angle = Math.atan2(dy, dx);
176
- const force = (params.mouseRadius - distance) / params.mouseRadius * params.mouseStrength;
177
- this.vx += Math.cos(angle) * force * 0.1;
178
- this.vy += Math.sin(angle) * force * 0.1;
179
  }
 
180
  }
181
-
182
- // Update position
183
- this.x += this.vx;
184
- this.y += this.vy;
185
-
186
- // Apply boundary conditions (wrap around)
187
- if (this.x < 0) this.x = canvas.width;
188
- if (this.x > canvas.width) this.x = 0;
189
- if (this.y < 0) this.y = canvas.height;
190
- if (this.y > canvas.height) this.y = 0;
191
-
192
- // Apply friction
193
- this.vx *= 0.98;
194
- this.vy *= 0.98;
195
-
196
- // Limit speed
197
- const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
198
- const maxSpeed = 3;
199
- if (speed > maxSpeed) {
200
- this.vx = (this.vx / speed) * maxSpeed;
201
- this.vy = (this.vy / speed) * maxSpeed;
202
- }
203
-
204
- // Update pulse phase for color animation
205
- this.pulsePhase += 0.02;
206
  }
207
 
208
- flock(particles) {
209
- let alignmentX = 0;
210
- let alignmentY = 0;
211
- let cohesionX = 0;
212
- let cohesionY = 0;
213
- let separationX = 0;
214
- let separationY = 0;
215
 
216
- let neighborCount = 0;
217
- const perceptionRadius = 100;
 
 
 
218
 
219
- for (let other of particles) {
220
- if (other === this) continue;
221
-
222
- const dx = other.x - this.x;
223
- const dy = other.y - this.y;
224
- const distance = Math.sqrt(dx * dx + dy * dy);
225
-
226
- if (distance < perceptionRadius) {
227
- neighborCount++;
228
-
229
- // Alignment: steer toward average heading of neighbors
230
- alignmentX += other.vx;
231
- alignmentY += other.vy;
232
-
233
- // Cohesion: steer toward average position of neighbors
234
- cohesionX += other.x;
235
- cohesionY += other.y;
236
-
237
- // Separation: avoid crowding neighbors
238
- if (distance < 30) {
239
- separationX -= dx / distance;
240
- separationY -= dy / distance;
241
- }
242
- }
243
- }
244
-
245
- if (neighborCount > 0) {
246
- // Apply alignment
247
- alignmentX /= neighborCount;
248
- alignmentY /= neighborCount;
249
- const alignMag = Math.sqrt(alignmentX * alignmentX + alignmentY * alignmentY);
250
- if (alignMag > 0) {
251
- alignmentX = (alignmentX / alignMag) * params.alignment;
252
- alignmentY = (alignmentY / alignMag) * params.alignment;
253
- }
254
-
255
- // Apply cohesion
256
- cohesionX /= neighborCount;
257
- cohesionY /= neighborCount;
258
- cohesionX -= this.x;
259
- cohesionY -= this.y;
260
- const cohereMag = Math.sqrt(cohesionX * cohesionX + cohesionY * cohesionY);
261
- if (cohereMag > 0) {
262
- cohesionX = (cohesionX / cohereMag) * params.cohesion;
263
- cohesionY = (cohesionY / cohereMag) * params.cohesion;
264
- }
265
-
266
- // Apply separation
267
- const separateMag = Math.sqrt(separationX * separationX + separationY * separationY);
268
- if (separateMag > 0) {
269
- separationX = (separationX / separateMag) * params.separation;
270
- separationY = (separationY / separateMag) * params.separation;
271
- }
272
-
273
- // Combine all forces
274
- this.vx += alignmentX + cohesionX + separationX;
275
- this.vy += alignmentY + cohesionY + separationY;
276
- }
277
  }
278
 
279
- draw(ctx) {
280
- // Determine color based on mode
281
- let color;
282
- const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
283
-
284
- switch(params.colorMode) {
285
- case 'velocity':
286
- color = `hsl(${(speed * 50) % 360}, 100%, 50%)`;
287
- break;
288
- case 'distance':
289
- const avgDist = this.calculateAverageDistance();
290
- color = `hsl(${(avgDist * 0.5) % 360}, 100%, 50%)`;
291
- break;
292
- case 'rainbow':
293
- color = `hsl(${(this.baseHue + Date.now() * 0.01) % 360}, 100%, 50%)`;
294
- break;
295
- case 'pulse':
296
- const pulseValue = Math.sin(this.pulsePhase) * 0.5 + 0.5;
297
- color = `hsl(${this.baseHue}, 100%, ${pulseValue * 50 + 30}%)`;
298
- break;
299
- default:
300
- color = this.color;
301
- }
302
-
303
- // Draw trail
304
- if (this.history.length > 1) {
305
- ctx.beginPath();
306
- ctx.moveTo(this.history[0].x, this.history[0].y);
307
-
308
- for (let i = 1; i < this.history.length; i++) {
309
- const alpha = i / this.history.length * params.trailLength;
310
- ctx.strokeStyle = color.replace(')', `, ${alpha})`).replace('hsl', 'hsla');
311
- ctx.lineWidth = this.size * 0.7;
312
- ctx.lineTo(this.history[i].x, this.history[i].y);
313
- ctx.stroke();
314
- ctx.beginPath();
315
- ctx.moveTo(this.history[i].x, this.history[i].y);
316
- }
317
- }
318
-
319
- // Draw particle
320
- ctx.beginPath();
321
- ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
322
- ctx.fillStyle = color;
323
- ctx.fill();
324
  }
325
 
326
- calculateAverageDistance() {
327
- let totalDist = 0;
328
- let count = 0;
329
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  for (let i = 1; i < this.history.length; i++) {
331
- const dx = this.history[i].x - this.history[i-1].x;
332
- const dy = this.history[i].y - this.history[i-1].y;
333
- totalDist += Math.sqrt(dx * dx + dy * dy);
334
- count++;
335
  }
336
-
337
- return count > 0 ? totalDist / count : 0;
 
 
 
 
 
 
 
338
  }
 
 
 
 
 
 
339
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
 
341
- // Simulation state
342
- let particles = [];
343
- let lastTime = 0;
344
- let frameCount = 0;
345
- let fps = 0;
346
 
347
- function initSimulation() {
348
- particles = [];
349
- for (let i = 0; i < params.particleCount; i++) {
350
- particles.push(new Particle());
 
 
 
 
 
351
  }
352
  }
353
 
354
- function resetSimulation() {
355
- initSimulation();
 
 
356
  }
357
 
358
- function animate(currentTime) {
359
- // Calculate FPS
360
- frameCount++;
361
- if (currentTime - lastTime >= 1000) {
362
- fps = frameCount;
363
- frameCount = 0;
364
- lastTime = currentTime;
 
 
 
 
365
  }
366
-
367
- // Clear canvas with fade effect
368
- ctx.fillStyle = `rgba(0, 0, 0, ${params.trailLength})`;
369
- ctx.fillRect(0, 0, canvas.width, canvas.height);
370
-
371
- // Update and draw all particles
372
- for (let particle of particles) {
373
- particle.update(particles);
374
- particle.draw(ctx);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  }
376
-
377
- // Draw FPS counter
378
- ctx.fillStyle = 'white';
379
- ctx.font = '14px Arial';
380
- ctx.fillText(`FPS: ${fps}`, 10, 20);
381
- ctx.fillText(`Particles: ${particles.length}`, 10, 40);
382
-
383
- requestAnimationFrame(animate);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  }
385
-
386
- // Handle window resize
387
- window.addEventListener('resize', () => {
388
- canvas.width = window.innerWidth;
389
- canvas.height = window.innerHeight;
390
- });
391
-
392
- // Start simulation
393
- initSimulation();
394
- animate();
395
  });
 
 
 
 
 
396
  </script>
397
- <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=raayraay/hypnotic-flocking" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body>
398
- </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Watch Whales Think 🐳</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <style>
9
+ * { margin: 0; padding: 0; box-sizing: border-box; }
10
  body {
 
11
  overflow: hidden;
12
  background: #000;
13
+ font-family: 'Inter', system-ui, sans-serif;
14
  }
15
+ canvas { display: block; position: fixed; top: 0; left: 0; }
16
+
17
+ /* Glassmorphism controls */
18
  .controls {
19
+ position: fixed;
20
  top: 20px;
21
  left: 20px;
22
+ padding: 24px;
23
+ background: rgba(0, 0, 0, 0.6);
24
+ backdrop-filter: blur(20px);
25
+ border-radius: 20px;
26
+ border: 1px solid rgba(255, 255, 255, 0.1);
27
+ color: white;
28
+ z-index: 100;
29
+ min-width: 280px;
30
+ transition: opacity 0.3s, transform 0.3s;
31
+ }
32
+
33
+ .controls.hidden {
34
+ opacity: 0;
35
+ pointer-events: none;
36
+ transform: translateX(-100%);
37
+ }
38
+
39
+ .controls h1 {
40
+ font-size: 1.5rem;
41
+ font-weight: 700;
42
+ margin-bottom: 4px;
43
+ background: linear-gradient(135deg, #00d9ff, #00ff88);
44
+ -webkit-background-clip: text;
45
+ -webkit-text-fill-color: transparent;
46
+ }
47
+
48
+ .controls .tagline {
49
+ font-size: 0.85rem;
50
+ color: rgba(255, 255, 255, 0.6);
51
+ margin-bottom: 20px;
52
+ }
53
+
54
+ .slider-group { margin-bottom: 16px; }
55
+ .slider-group label {
56
+ display: block;
57
+ font-size: 0.75rem;
58
+ text-transform: uppercase;
59
+ letter-spacing: 0.1em;
60
+ color: rgba(255, 255, 255, 0.7);
61
+ margin-bottom: 6px;
62
+ }
63
+
64
+ input[type="range"] {
65
+ width: 100%;
66
+ height: 6px;
67
+ border-radius: 3px;
68
+ background: rgba(255, 255, 255, 0.1);
69
+ appearance: none;
70
+ cursor: pointer;
71
+ }
72
+
73
+ input[type="range"]::-webkit-slider-thumb {
74
+ appearance: none;
75
+ width: 18px;
76
+ height: 18px;
77
+ border-radius: 50%;
78
+ background: linear-gradient(135deg, #00d9ff, #00ff88);
79
+ cursor: pointer;
80
+ box-shadow: 0 0 15px rgba(0, 217, 255, 0.5);
81
+ }
82
+
83
+ .btn {
84
+ width: 100%;
85
+ padding: 12px;
86
+ border: none;
87
+ border-radius: 12px;
88
+ font-weight: 600;
89
+ cursor: pointer;
90
+ transition: all 0.2s;
91
+ margin-top: 8px;
92
+ font-size: 0.9rem;
93
+ }
94
+
95
+ .btn-primary {
96
+ background: linear-gradient(135deg, #00d9ff, #00ff88);
97
+ color: #000;
98
+ }
99
+
100
+ .btn-secondary {
101
+ background: rgba(255, 255, 255, 0.1);
102
+ color: white;
103
+ border: 1px solid rgba(255, 255, 255, 0.2);
104
+ }
105
+
106
+ .btn:hover { transform: scale(1.02); }
107
+ .btn:active { transform: scale(0.98); }
108
+
109
+ .btn-audio.active {
110
+ background: linear-gradient(135deg, #ff0080, #ff6600);
111
+ animation: pulse 1s infinite;
112
+ }
113
+
114
+ @keyframes pulse {
115
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(255, 0, 128, 0.5); }
116
+ 50% { box-shadow: 0 0 20px 5px rgba(255, 0, 128, 0.3); }
117
+ }
118
+
119
+ /* Floating toggle button */
120
+ .toggle-btn {
121
+ position: fixed;
122
+ bottom: 20px;
123
+ right: 20px;
124
+ width: 50px;
125
+ height: 50px;
126
+ border-radius: 50%;
127
+ background: rgba(0, 0, 0, 0.6);
128
+ backdrop-filter: blur(10px);
129
+ border: 1px solid rgba(255, 255, 255, 0.2);
130
+ color: white;
131
+ font-size: 1.5rem;
132
+ cursor: pointer;
133
  z-index: 100;
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: center;
137
+ transition: all 0.2s;
138
+ }
139
+
140
+ .toggle-btn:hover {
141
+ background: rgba(0, 217, 255, 0.3);
142
+ transform: scale(1.1);
143
+ }
144
+
145
+ /* Audio visualizer indicator */
146
+ .audio-visualizer {
147
+ position: fixed;
148
+ bottom: 20px;
149
+ left: 50%;
150
+ transform: translateX(-50%);
151
+ display: flex;
152
+ gap: 4px;
153
+ opacity: 0;
154
+ transition: opacity 0.3s;
155
+ }
156
+
157
+ .audio-visualizer.active { opacity: 1; }
158
+
159
+ .audio-bar {
160
+ width: 4px;
161
+ height: 20px;
162
+ background: linear-gradient(to top, #00d9ff, #00ff88);
163
+ border-radius: 2px;
164
+ transform-origin: bottom;
165
+ }
166
+
167
+ /* Recording indicator */
168
+ .recording-indicator {
169
+ position: fixed;
170
+ top: 20px;
171
+ right: 20px;
172
+ display: flex;
173
+ align-items: center;
174
+ gap: 8px;
175
+ padding: 10px 16px;
176
+ background: rgba(255, 0, 0, 0.8);
177
+ border-radius: 20px;
178
  color: white;
179
+ font-weight: 600;
180
+ font-size: 0.85rem;
181
+ opacity: 0;
182
+ transition: opacity 0.3s;
183
+ z-index: 100;
184
  }
185
+
186
+ .recording-indicator.active { opacity: 1; }
187
+
188
+ .recording-dot {
189
+ width: 10px;
190
+ height: 10px;
191
  border-radius: 50%;
192
+ background: white;
193
+ animation: blink 1s infinite;
194
+ }
195
+
196
+ @keyframes blink {
197
+ 0%, 100% { opacity: 1; }
198
+ 50% { opacity: 0.3; }
199
  }
200
  </style>
201
  </head>
202
  <body>
203
+ <canvas id="canvas"></canvas>
204
+
205
+ <!-- Controls Panel -->
206
+ <div class="controls" id="controls">
207
+ <h1>🐳 Hypnotic Flocking</h1>
208
+ <p class="tagline">Watch whales think</p>
209
+
210
+ <div class="slider-group">
211
+ <label>Chaos Level</label>
212
+ <input type="range" id="chaos" min="0" max="100" value="30">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  </div>
214
+
215
+ <div class="slider-group">
216
+ <label>Whale Count</label>
217
+ <input type="range" id="count" min="50" max="1000" value="500">
218
+ </div>
219
+
220
+ <div class="slider-group">
221
+ <label>Trail Glow</label>
222
+ <input type="range" id="trail" min="0" max="100" value="70">
223
+ </div>
224
+
225
+ <button class="btn btn-secondary btn-audio" id="audioBtn">
226
+ 🎤 Audio Reactive
227
+ </button>
228
+
229
+ <button class="btn btn-primary" id="recordBtn">
230
+ 📹 Record MP4
231
+ </button>
232
+
233
+ <button class="btn btn-secondary" id="resetBtn">
234
+ ↻ Reset
235
+ </button>
236
+ </div>
237
+
238
+ <!-- Toggle Button -->
239
+ <button class="toggle-btn" id="toggleBtn" title="Toggle Controls">⚙️</button>
240
+
241
+ <!-- Audio Visualizer -->
242
+ <div class="audio-visualizer" id="audioVisualizer">
243
+ <div class="audio-bar"></div>
244
+ <div class="audio-bar"></div>
245
+ <div class="audio-bar"></div>
246
+ <div class="audio-bar"></div>
247
+ <div class="audio-bar"></div>
248
+ <div class="audio-bar"></div>
249
+ <div class="audio-bar"></div>
250
+ <div class="audio-bar"></div>
251
+ </div>
252
+
253
+ <!-- Recording Indicator -->
254
+ <div class="recording-indicator" id="recordingIndicator">
255
+ <div class="recording-dot"></div>
256
+ <span>Recording...</span>
257
  </div>
258
 
259
  <script>
260
+ // Canvas setup
261
+ const canvas = document.getElementById('canvas');
262
+ const ctx = canvas.getContext('2d');
263
+ let width, height;
264
+
265
+ function resize() {
266
+ width = canvas.width = window.innerWidth;
267
+ height = canvas.height = window.innerHeight;
268
+ }
269
+ resize();
270
+ window.addEventListener('resize', resize);
271
+
272
+ // State
273
+ let particles = [];
274
+ let audioContext = null;
275
+ let analyser = null;
276
+ let audioData = null;
277
+ let isAudioActive = false;
278
+ let audioLevel = 0;
279
+ let mediaRecorder = null;
280
+ let recordedChunks = [];
281
+ let isRecording = false;
282
+
283
+ // Controls
284
+ const chaosSlider = document.getElementById('chaos');
285
+ const countSlider = document.getElementById('count');
286
+ const trailSlider = document.getElementById('trail');
287
+ const audioBtn = document.getElementById('audioBtn');
288
+ const recordBtn = document.getElementById('recordBtn');
289
+ const resetBtn = document.getElementById('resetBtn');
290
+ const toggleBtn = document.getElementById('toggleBtn');
291
+ const controlsPanel = document.getElementById('controls');
292
+ const audioVisualizer = document.getElementById('audioVisualizer');
293
+ const recordingIndicator = document.getElementById('recordingIndicator');
294
+ const audioBars = audioVisualizer.querySelectorAll('.audio-bar');
295
+
296
+ // Particle class
297
+ class Particle {
298
+ constructor() {
299
+ this.x = Math.random() * width;
300
+ this.y = Math.random() * height;
301
+ this.vx = (Math.random() - 0.5) * 4;
302
+ this.vy = (Math.random() - 0.5) * 4;
303
+ this.history = [];
304
+ this.hue = Math.random() * 60 + 140; // Teal-green range
305
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
 
307
+ update(particles, chaos) {
308
+ // Boids algorithm
309
+ let alignX = 0, alignY = 0;
310
+ let cohesionX = 0, cohesionY = 0;
311
+ let separationX = 0, separationY = 0;
312
+ let neighbors = 0;
 
313
 
314
+ const perception = 80 + chaos * 0.5;
315
+ const chaosMultiplier = 1 + (chaos / 100) * 2;
 
 
 
 
 
 
 
 
316
 
317
+ for (const other of particles) {
318
+ const dx = other.x - this.x;
319
+ const dy = other.y - this.y;
320
+ const dist = Math.sqrt(dx * dx + dy * dy);
 
 
321
 
322
+ if (dist > 0 && dist < perception) {
323
+ alignX += other.vx;
324
+ alignY += other.vy;
325
+ cohesionX += other.x;
326
+ cohesionY += other.y;
 
 
 
327
 
328
+ if (dist < 30) {
329
+ separationX -= dx / dist;
330
+ separationY -= dy / dist;
 
 
331
  }
332
+ neighbors++;
333
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  }
335
 
336
+ if (neighbors > 0) {
337
+ // Alignment
338
+ alignX /= neighbors;
339
+ alignY /= neighbors;
340
+ this.vx += (alignX - this.vx) * 0.05 * chaosMultiplier;
341
+ this.vy += (alignY - this.vy) * 0.05 * chaosMultiplier;
 
342
 
343
+ // Cohesion
344
+ cohesionX /= neighbors;
345
+ cohesionY /= neighbors;
346
+ this.vx += (cohesionX - this.x) * 0.001;
347
+ this.vy += (cohesionY - this.y) * 0.001;
348
 
349
+ // Separation
350
+ this.vx += separationX * 0.5;
351
+ this.vy += separationY * 0.5;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  }
353
 
354
+ // Audio reactivity
355
+ if (isAudioActive && audioLevel > 0) {
356
+ const audioForce = audioLevel * 0.3;
357
+ this.vx += (Math.random() - 0.5) * audioForce;
358
+ this.vy += (Math.random() - 0.5) * audioForce;
359
+ this.hue = 140 + audioLevel * 200; // Shift colors with audio
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  }
361
 
362
+ // Speed limit
363
+ const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
364
+ const maxSpeed = 4 + (chaos / 100) * 4;
365
+ if (speed > maxSpeed) {
366
+ this.vx = (this.vx / speed) * maxSpeed;
367
+ this.vy = (this.vy / speed) * maxSpeed;
368
+ }
369
+
370
+ // Move
371
+ this.x += this.vx;
372
+ this.y += this.vy;
373
+
374
+ // Wrap edges
375
+ if (this.x < 0) this.x = width;
376
+ if (this.x > width) this.x = 0;
377
+ if (this.y < 0) this.y = height;
378
+ if (this.y > height) this.y = 0;
379
+
380
+ // Store history for trails
381
+ this.history.push({ x: this.x, y: this.y });
382
+ const maxHistory = Math.floor(trailSlider.value / 2) + 5;
383
+ if (this.history.length > maxHistory) {
384
+ this.history.shift();
385
+ }
386
+ }
387
+
388
+ draw() {
389
+ // Draw trail
390
+ if (this.history.length > 1) {
391
+ ctx.beginPath();
392
+ ctx.moveTo(this.history[0].x, this.history[0].y);
393
  for (let i = 1; i < this.history.length; i++) {
394
+ ctx.lineTo(this.history[i].x, this.history[i].y);
 
 
 
395
  }
396
+ const gradient = ctx.createLinearGradient(
397
+ this.history[0].x, this.history[0].y,
398
+ this.x, this.y
399
+ );
400
+ gradient.addColorStop(0, `hsla(${this.hue}, 100%, 50%, 0)`);
401
+ gradient.addColorStop(1, `hsla(${this.hue}, 100%, 60%, 0.8)`);
402
+ ctx.strokeStyle = gradient;
403
+ ctx.lineWidth = 2;
404
+ ctx.stroke();
405
  }
406
+
407
+ // Draw head
408
+ ctx.beginPath();
409
+ ctx.arc(this.x, this.y, 3, 0, Math.PI * 2);
410
+ ctx.fillStyle = `hsl(${this.hue}, 100%, 70%)`;
411
+ ctx.fill();
412
  }
413
+ }
414
+
415
+ // Initialize particles
416
+ function initParticles(count) {
417
+ particles = [];
418
+ for (let i = 0; i < count; i++) {
419
+ particles.push(new Particle());
420
+ }
421
+ }
422
+
423
+ initParticles(500);
424
+
425
+ // Animation loop
426
+ function animate() {
427
+ // Fade effect for trails
428
+ ctx.fillStyle = `rgba(0, 0, 0, ${1 - trailSlider.value / 100})`;
429
+ ctx.fillRect(0, 0, width, height);
430
 
431
+ const chaos = parseFloat(chaosSlider.value);
 
 
 
 
432
 
433
+ // Update audio level
434
+ if (isAudioActive && analyser) {
435
+ analyser.getByteFrequencyData(audioData);
436
+ audioLevel = audioData.reduce((a, b) => a + b) / audioData.length / 255;
437
+
438
+ // Update visualizer bars
439
+ for (let i = 0; i < audioBars.length; i++) {
440
+ const value = audioData[i * 4] / 255;
441
+ audioBars[i].style.transform = `scaleY(${0.3 + value * 2})`;
442
  }
443
  }
444
 
445
+ // Update and draw particles
446
+ for (const p of particles) {
447
+ p.update(particles, chaos);
448
+ p.draw();
449
  }
450
 
451
+ requestAnimationFrame(animate);
452
+ }
453
+
454
+ animate();
455
+
456
+ // Event listeners
457
+ countSlider.addEventListener('input', () => {
458
+ const targetCount = parseInt(countSlider.value);
459
+ if (particles.length < targetCount) {
460
+ while (particles.length < targetCount) {
461
+ particles.push(new Particle());
462
  }
463
+ } else {
464
+ particles.length = targetCount;
465
+ }
466
+ });
467
+
468
+ resetBtn.addEventListener('click', () => {
469
+ initParticles(parseInt(countSlider.value));
470
+ });
471
+
472
+ toggleBtn.addEventListener('click', () => {
473
+ controlsPanel.classList.toggle('hidden');
474
+ });
475
+
476
+ // Audio reactive mode
477
+ audioBtn.addEventListener('click', async () => {
478
+ if (!isAudioActive) {
479
+ try {
480
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
481
+ audioContext = new AudioContext();
482
+ const source = audioContext.createMediaStreamSource(stream);
483
+ analyser = audioContext.createAnalyser();
484
+ analyser.fftSize = 64;
485
+ source.connect(analyser);
486
+ audioData = new Uint8Array(analyser.frequencyBinCount);
487
+ isAudioActive = true;
488
+ audioBtn.classList.add('active');
489
+ audioBtn.textContent = '🎤 Audio ON';
490
+ audioVisualizer.classList.add('active');
491
+ } catch (e) {
492
+ console.error('Microphone access denied:', e);
493
+ alert('Please allow microphone access for audio-reactive mode');
494
  }
495
+ } else {
496
+ isAudioActive = false;
497
+ audioLevel = 0;
498
+ if (audioContext) {
499
+ audioContext.close();
500
+ audioContext = null;
501
+ }
502
+ audioBtn.classList.remove('active');
503
+ audioBtn.textContent = '🎤 Audio Reactive';
504
+ audioVisualizer.classList.remove('active');
505
+ }
506
+ });
507
+
508
+ // MP4 Recording
509
+ recordBtn.addEventListener('click', async () => {
510
+ if (!isRecording) {
511
+ try {
512
+ const stream = canvas.captureStream(60);
513
+ mediaRecorder = new MediaRecorder(stream, {
514
+ mimeType: 'video/webm;codecs=vp9',
515
+ videoBitsPerSecond: 8000000
516
+ });
517
+
518
+ recordedChunks = [];
519
+ mediaRecorder.ondataavailable = (e) => {
520
+ if (e.data.size > 0) recordedChunks.push(e.data);
521
+ };
522
+
523
+ mediaRecorder.onstop = () => {
524
+ const blob = new Blob(recordedChunks, { type: 'video/webm' });
525
+ const url = URL.createObjectURL(blob);
526
+ const a = document.createElement('a');
527
+ a.href = url;
528
+ a.download = 'whale-dreams.webm';
529
+ a.click();
530
+ URL.revokeObjectURL(url);
531
+ };
532
+
533
+ mediaRecorder.start();
534
+ isRecording = true;
535
+ recordBtn.textContent = '⏹️ Stop Recording';
536
+ recordBtn.classList.remove('btn-primary');
537
+ recordBtn.classList.add('btn-secondary');
538
+ recordingIndicator.classList.add('active');
539
+
540
+ // Auto-stop after 30 seconds
541
+ setTimeout(() => {
542
+ if (isRecording) {
543
+ recordBtn.click();
544
+ }
545
+ }, 30000);
546
+ } catch (e) {
547
+ console.error('Recording failed:', e);
548
+ alert('Recording not supported in this browser');
549
+ }
550
+ } else {
551
+ mediaRecorder.stop();
552
+ isRecording = false;
553
+ recordBtn.textContent = '📹 Record MP4';
554
+ recordBtn.classList.add('btn-primary');
555
+ recordBtn.classList.remove('btn-secondary');
556
+ recordingIndicator.classList.remove('active');
557
+ }
558
+ });
559
+
560
+ // Keyboard shortcuts
561
+ document.addEventListener('keydown', (e) => {
562
+ if (e.key === 'f' || e.key === 'F') {
563
+ controlsPanel.classList.toggle('hidden');
564
+ }
565
+ if (e.key === ' ') {
566
+ audioBtn.click();
567
+ }
568
+ if (e.key === 'r' || e.key === 'R') {
569
+ recordBtn.click();
570
  }
 
 
 
 
 
 
 
 
 
 
571
  });
572
+
573
+ // Start in fullscreen mode after 5 seconds
574
+ setTimeout(() => {
575
+ controlsPanel.classList.add('hidden');
576
+ }, 5000);
577
  </script>
578
+ </body>
579
+ </html>