hypnotic-flocking / index.html
raayraay99
✨ Viral upgrade: audio-reactive, fullscreen, video export
24883ad
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Watch Whales Think 🐳</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
overflow: hidden;
background: #000;
font-family: 'Inter', system-ui, sans-serif;
}
canvas { display: block; position: fixed; top: 0; left: 0; }
/* Glassmorphism controls */
.controls {
position: fixed;
top: 20px;
left: 20px;
padding: 24px;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(20px);
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
color: white;
z-index: 100;
min-width: 280px;
transition: opacity 0.3s, transform 0.3s;
}
.controls.hidden {
opacity: 0;
pointer-events: none;
transform: translateX(-100%);
}
.controls h1 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 4px;
background: linear-gradient(135deg, #00d9ff, #00ff88);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.controls .tagline {
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 20px;
}
.slider-group { margin-bottom: 16px; }
.slider-group label {
display: block;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 6px;
}
input[type="range"] {
width: 100%;
height: 6px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.1);
appearance: none;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: linear-gradient(135deg, #00d9ff, #00ff88);
cursor: pointer;
box-shadow: 0 0 15px rgba(0, 217, 255, 0.5);
}
.btn {
width: 100%;
padding: 12px;
border: none;
border-radius: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
margin-top: 8px;
font-size: 0.9rem;
}
.btn-primary {
background: linear-gradient(135deg, #00d9ff, #00ff88);
color: #000;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.btn:hover { transform: scale(1.02); }
.btn:active { transform: scale(0.98); }
.btn-audio.active {
background: linear-gradient(135deg, #ff0080, #ff6600);
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(255, 0, 128, 0.5); }
50% { box-shadow: 0 0 20px 5px rgba(255, 0, 128, 0.3); }
}
/* Floating toggle button */
.toggle-btn {
position: fixed;
bottom: 20px;
right: 20px;
width: 50px;
height: 50px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
font-size: 1.5rem;
cursor: pointer;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.toggle-btn:hover {
background: rgba(0, 217, 255, 0.3);
transform: scale(1.1);
}
/* Audio visualizer indicator */
.audio-visualizer {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.3s;
}
.audio-visualizer.active { opacity: 1; }
.audio-bar {
width: 4px;
height: 20px;
background: linear-gradient(to top, #00d9ff, #00ff88);
border-radius: 2px;
transform-origin: bottom;
}
/* Recording indicator */
.recording-indicator {
position: fixed;
top: 20px;
right: 20px;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: rgba(255, 0, 0, 0.8);
border-radius: 20px;
color: white;
font-weight: 600;
font-size: 0.85rem;
opacity: 0;
transition: opacity 0.3s;
z-index: 100;
}
.recording-indicator.active { opacity: 1; }
.recording-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: white;
animation: blink 1s infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<!-- Controls Panel -->
<div class="controls" id="controls">
<h1>🐳 Hypnotic Flocking</h1>
<p class="tagline">Watch whales think</p>
<div class="slider-group">
<label>Chaos Level</label>
<input type="range" id="chaos" min="0" max="100" value="30">
</div>
<div class="slider-group">
<label>Whale Count</label>
<input type="range" id="count" min="50" max="1000" value="500">
</div>
<div class="slider-group">
<label>Trail Glow</label>
<input type="range" id="trail" min="0" max="100" value="70">
</div>
<button class="btn btn-secondary btn-audio" id="audioBtn">
🎤 Audio Reactive
</button>
<button class="btn btn-primary" id="recordBtn">
📹 Record MP4
</button>
<button class="btn btn-secondary" id="resetBtn">
↻ Reset
</button>
</div>
<!-- Toggle Button -->
<button class="toggle-btn" id="toggleBtn" title="Toggle Controls">⚙️</button>
<!-- Audio Visualizer -->
<div class="audio-visualizer" id="audioVisualizer">
<div class="audio-bar"></div>
<div class="audio-bar"></div>
<div class="audio-bar"></div>
<div class="audio-bar"></div>
<div class="audio-bar"></div>
<div class="audio-bar"></div>
<div class="audio-bar"></div>
<div class="audio-bar"></div>
</div>
<!-- Recording Indicator -->
<div class="recording-indicator" id="recordingIndicator">
<div class="recording-dot"></div>
<span>Recording...</span>
</div>
<script>
// Canvas setup
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let width, height;
function resize() {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
// State
let particles = [];
let audioContext = null;
let analyser = null;
let audioData = null;
let isAudioActive = false;
let audioLevel = 0;
let mediaRecorder = null;
let recordedChunks = [];
let isRecording = false;
// Controls
const chaosSlider = document.getElementById('chaos');
const countSlider = document.getElementById('count');
const trailSlider = document.getElementById('trail');
const audioBtn = document.getElementById('audioBtn');
const recordBtn = document.getElementById('recordBtn');
const resetBtn = document.getElementById('resetBtn');
const toggleBtn = document.getElementById('toggleBtn');
const controlsPanel = document.getElementById('controls');
const audioVisualizer = document.getElementById('audioVisualizer');
const recordingIndicator = document.getElementById('recordingIndicator');
const audioBars = audioVisualizer.querySelectorAll('.audio-bar');
// Particle class
class Particle {
constructor() {
this.x = Math.random() * width;
this.y = Math.random() * height;
this.vx = (Math.random() - 0.5) * 4;
this.vy = (Math.random() - 0.5) * 4;
this.history = [];
this.hue = Math.random() * 60 + 140; // Teal-green range
}
update(particles, chaos) {
// Boids algorithm
let alignX = 0, alignY = 0;
let cohesionX = 0, cohesionY = 0;
let separationX = 0, separationY = 0;
let neighbors = 0;
const perception = 80 + chaos * 0.5;
const chaosMultiplier = 1 + (chaos / 100) * 2;
for (const other of particles) {
const dx = other.x - this.x;
const dy = other.y - this.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0 && dist < perception) {
alignX += other.vx;
alignY += other.vy;
cohesionX += other.x;
cohesionY += other.y;
if (dist < 30) {
separationX -= dx / dist;
separationY -= dy / dist;
}
neighbors++;
}
}
if (neighbors > 0) {
// Alignment
alignX /= neighbors;
alignY /= neighbors;
this.vx += (alignX - this.vx) * 0.05 * chaosMultiplier;
this.vy += (alignY - this.vy) * 0.05 * chaosMultiplier;
// Cohesion
cohesionX /= neighbors;
cohesionY /= neighbors;
this.vx += (cohesionX - this.x) * 0.001;
this.vy += (cohesionY - this.y) * 0.001;
// Separation
this.vx += separationX * 0.5;
this.vy += separationY * 0.5;
}
// Audio reactivity
if (isAudioActive && audioLevel > 0) {
const audioForce = audioLevel * 0.3;
this.vx += (Math.random() - 0.5) * audioForce;
this.vy += (Math.random() - 0.5) * audioForce;
this.hue = 140 + audioLevel * 200; // Shift colors with audio
}
// Speed limit
const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
const maxSpeed = 4 + (chaos / 100) * 4;
if (speed > maxSpeed) {
this.vx = (this.vx / speed) * maxSpeed;
this.vy = (this.vy / speed) * maxSpeed;
}
// Move
this.x += this.vx;
this.y += this.vy;
// Wrap edges
if (this.x < 0) this.x = width;
if (this.x > width) this.x = 0;
if (this.y < 0) this.y = height;
if (this.y > height) this.y = 0;
// Store history for trails
this.history.push({ x: this.x, y: this.y });
const maxHistory = Math.floor(trailSlider.value / 2) + 5;
if (this.history.length > maxHistory) {
this.history.shift();
}
}
draw() {
// Draw trail
if (this.history.length > 1) {
ctx.beginPath();
ctx.moveTo(this.history[0].x, this.history[0].y);
for (let i = 1; i < this.history.length; i++) {
ctx.lineTo(this.history[i].x, this.history[i].y);
}
const gradient = ctx.createLinearGradient(
this.history[0].x, this.history[0].y,
this.x, this.y
);
gradient.addColorStop(0, `hsla(${this.hue}, 100%, 50%, 0)`);
gradient.addColorStop(1, `hsla(${this.hue}, 100%, 60%, 0.8)`);
ctx.strokeStyle = gradient;
ctx.lineWidth = 2;
ctx.stroke();
}
// Draw head
ctx.beginPath();
ctx.arc(this.x, this.y, 3, 0, Math.PI * 2);
ctx.fillStyle = `hsl(${this.hue}, 100%, 70%)`;
ctx.fill();
}
}
// Initialize particles
function initParticles(count) {
particles = [];
for (let i = 0; i < count; i++) {
particles.push(new Particle());
}
}
initParticles(500);
// Animation loop
function animate() {
// Fade effect for trails
ctx.fillStyle = `rgba(0, 0, 0, ${1 - trailSlider.value / 100})`;
ctx.fillRect(0, 0, width, height);
const chaos = parseFloat(chaosSlider.value);
// Update audio level
if (isAudioActive && analyser) {
analyser.getByteFrequencyData(audioData);
audioLevel = audioData.reduce((a, b) => a + b) / audioData.length / 255;
// Update visualizer bars
for (let i = 0; i < audioBars.length; i++) {
const value = audioData[i * 4] / 255;
audioBars[i].style.transform = `scaleY(${0.3 + value * 2})`;
}
}
// Update and draw particles
for (const p of particles) {
p.update(particles, chaos);
p.draw();
}
requestAnimationFrame(animate);
}
animate();
// Event listeners
countSlider.addEventListener('input', () => {
const targetCount = parseInt(countSlider.value);
if (particles.length < targetCount) {
while (particles.length < targetCount) {
particles.push(new Particle());
}
} else {
particles.length = targetCount;
}
});
resetBtn.addEventListener('click', () => {
initParticles(parseInt(countSlider.value));
});
toggleBtn.addEventListener('click', () => {
controlsPanel.classList.toggle('hidden');
});
// Audio reactive mode
audioBtn.addEventListener('click', async () => {
if (!isAudioActive) {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
audioContext = new AudioContext();
const source = audioContext.createMediaStreamSource(stream);
analyser = audioContext.createAnalyser();
analyser.fftSize = 64;
source.connect(analyser);
audioData = new Uint8Array(analyser.frequencyBinCount);
isAudioActive = true;
audioBtn.classList.add('active');
audioBtn.textContent = '🎤 Audio ON';
audioVisualizer.classList.add('active');
} catch (e) {
console.error('Microphone access denied:', e);
alert('Please allow microphone access for audio-reactive mode');
}
} else {
isAudioActive = false;
audioLevel = 0;
if (audioContext) {
audioContext.close();
audioContext = null;
}
audioBtn.classList.remove('active');
audioBtn.textContent = '🎤 Audio Reactive';
audioVisualizer.classList.remove('active');
}
});
// MP4 Recording
recordBtn.addEventListener('click', async () => {
if (!isRecording) {
try {
const stream = canvas.captureStream(60);
mediaRecorder = new MediaRecorder(stream, {
mimeType: 'video/webm;codecs=vp9',
videoBitsPerSecond: 8000000
});
recordedChunks = [];
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) recordedChunks.push(e.data);
};
mediaRecorder.onstop = () => {
const blob = new Blob(recordedChunks, { type: 'video/webm' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'whale-dreams.webm';
a.click();
URL.revokeObjectURL(url);
};
mediaRecorder.start();
isRecording = true;
recordBtn.textContent = '⏹️ Stop Recording';
recordBtn.classList.remove('btn-primary');
recordBtn.classList.add('btn-secondary');
recordingIndicator.classList.add('active');
// Auto-stop after 30 seconds
setTimeout(() => {
if (isRecording) {
recordBtn.click();
}
}, 30000);
} catch (e) {
console.error('Recording failed:', e);
alert('Recording not supported in this browser');
}
} else {
mediaRecorder.stop();
isRecording = false;
recordBtn.textContent = '📹 Record MP4';
recordBtn.classList.add('btn-primary');
recordBtn.classList.remove('btn-secondary');
recordingIndicator.classList.remove('active');
}
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'f' || e.key === 'F') {
controlsPanel.classList.toggle('hidden');
}
if (e.key === ' ') {
audioBtn.click();
}
if (e.key === 'r' || e.key === 'R') {
recordBtn.click();
}
});
// Start in fullscreen mode after 5 seconds
setTimeout(() => {
controlsPanel.classList.add('hidden');
}, 5000);
</script>
</body>
</html>