hypnotic-flocking / index.html
raayraay's picture
Add 2 files
7f8fcea verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hypnotic Flocking Simulation</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
margin: 0;
overflow: hidden;
background: #000;
}
canvas {
display: block;
}
.controls {
position: absolute;
top: 20px;
left: 20px;
z-index: 100;
color: white;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
font-family: 'Arial', sans-serif;
}
.particle {
position: absolute;
border-radius: 50%;
pointer-events: none;
}
</style>
</head>
<body>
<canvas id="flockCanvas"></canvas>
<div class="controls">
<h1 class="text-2xl font-bold mb-4 text-purple-400">Hypnotic Flocking</h1>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-cyan-300">Alignment</label>
<input type="range" id="alignment" min="0" max="2" step="0.01" value="1" class="w-full">
</div>
<div>
<label class="block text-sm font-medium text-cyan-300">Cohesion</label>
<input type="range" id="cohesion" min="0" max="2" step="0.01" value="1" class="w-full">
</div>
<div>
<label class="block text-sm font-medium text-cyan-300">Separation</label>
<input type="range" id="separation" min="0" max="2" step="0.01" value="1.5" class="w-full">
</div>
<div>
<label class="block text-sm font-medium text-cyan-300">Particle Count</label>
<input type="range" id="particleCount" min="50" max="1000" step="10" value="500" class="w-full">
</div>
<div>
<label class="block text-sm font-medium text-cyan-300">Trail Length</label>
<input type="range" id="trailLength" min="0" max="1" step="0.01" value="0.05" class="w-full">
</div>
<div>
<label class="block text-sm font-medium text-cyan-300">Color Mode</label>
<select id="colorMode" class="w-full bg-gray-800 text-white rounded p-1">
<option value="velocity">Velocity</option>
<option value="distance">Distance</option>
<option value="rainbow">Rainbow</option>
<option value="pulse">Pulse</option>
</select>
</div>
</div>
<button id="resetBtn" class="mt-4 px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded text-white">Reset Simulation</button>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const canvas = document.getElementById('flockCanvas');
const ctx = canvas.getContext('2d');
// Set canvas to full window size
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// Flocking parameters
let params = {
alignment: 1,
cohesion: 1,
separation: 1.5,
particleCount: 500,
trailLength: 0.05,
colorMode: 'velocity',
mouseInfluence: true,
mouseRadius: 100,
mouseStrength: 5
};
// Initialize controls
document.getElementById('alignment').addEventListener('input', (e) => {
params.alignment = parseFloat(e.target.value);
});
document.getElementById('cohesion').addEventListener('input', (e) => {
params.cohesion = parseFloat(e.target.value);
});
document.getElementById('separation').addEventListener('input', (e) => {
params.separation = parseFloat(e.target.value);
});
document.getElementById('particleCount').addEventListener('input', (e) => {
params.particleCount = parseInt(e.target.value);
resetSimulation();
});
document.getElementById('trailLength').addEventListener('input', (e) => {
params.trailLength = parseFloat(e.target.value);
});
document.getElementById('colorMode').addEventListener('change', (e) => {
params.colorMode = e.target.value;
});
document.getElementById('resetBtn').addEventListener('click', () => {
resetSimulation();
});
// Mouse position
let mouseX = null;
let mouseY = null;
canvas.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
});
canvas.addEventListener('mouseout', () => {
mouseX = null;
mouseY = null;
});
// Particle class
class Particle {
constructor() {
this.reset();
this.history = [];
this.maxHistory = 5;
}
reset() {
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
this.vx = (Math.random() - 0.5) * 2;
this.vy = (Math.random() - 0.5) * 2;
this.size = 2 + Math.random() * 3;
this.color = `hsl(${Math.random() * 360}, 100%, 50%)`;
this.baseHue = Math.random() * 360;
this.pulsePhase = Math.random() * Math.PI * 2;
}
update(particles) {
// Store position history for trails
this.history.unshift({x: this.x, y: this.y});
if (this.history.length > this.maxHistory) {
this.history.pop();
}
// Apply flocking rules
this.flock(particles);
// Apply mouse influence
if (mouseX !== null && mouseY !== null && params.mouseInfluence) {
const dx = mouseX - this.x;
const dy = mouseY - this.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < params.mouseRadius) {
const angle = Math.atan2(dy, dx);
const force = (params.mouseRadius - distance) / params.mouseRadius * params.mouseStrength;
this.vx += Math.cos(angle) * force * 0.1;
this.vy += Math.sin(angle) * force * 0.1;
}
}
// Update position
this.x += this.vx;
this.y += this.vy;
// Apply boundary conditions (wrap around)
if (this.x < 0) this.x = canvas.width;
if (this.x > canvas.width) this.x = 0;
if (this.y < 0) this.y = canvas.height;
if (this.y > canvas.height) this.y = 0;
// Apply friction
this.vx *= 0.98;
this.vy *= 0.98;
// Limit speed
const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
const maxSpeed = 3;
if (speed > maxSpeed) {
this.vx = (this.vx / speed) * maxSpeed;
this.vy = (this.vy / speed) * maxSpeed;
}
// Update pulse phase for color animation
this.pulsePhase += 0.02;
}
flock(particles) {
let alignmentX = 0;
let alignmentY = 0;
let cohesionX = 0;
let cohesionY = 0;
let separationX = 0;
let separationY = 0;
let neighborCount = 0;
const perceptionRadius = 100;
for (let other of particles) {
if (other === this) continue;
const dx = other.x - this.x;
const dy = other.y - this.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < perceptionRadius) {
neighborCount++;
// Alignment: steer toward average heading of neighbors
alignmentX += other.vx;
alignmentY += other.vy;
// Cohesion: steer toward average position of neighbors
cohesionX += other.x;
cohesionY += other.y;
// Separation: avoid crowding neighbors
if (distance < 30) {
separationX -= dx / distance;
separationY -= dy / distance;
}
}
}
if (neighborCount > 0) {
// Apply alignment
alignmentX /= neighborCount;
alignmentY /= neighborCount;
const alignMag = Math.sqrt(alignmentX * alignmentX + alignmentY * alignmentY);
if (alignMag > 0) {
alignmentX = (alignmentX / alignMag) * params.alignment;
alignmentY = (alignmentY / alignMag) * params.alignment;
}
// Apply cohesion
cohesionX /= neighborCount;
cohesionY /= neighborCount;
cohesionX -= this.x;
cohesionY -= this.y;
const cohereMag = Math.sqrt(cohesionX * cohesionX + cohesionY * cohesionY);
if (cohereMag > 0) {
cohesionX = (cohesionX / cohereMag) * params.cohesion;
cohesionY = (cohesionY / cohereMag) * params.cohesion;
}
// Apply separation
const separateMag = Math.sqrt(separationX * separationX + separationY * separationY);
if (separateMag > 0) {
separationX = (separationX / separateMag) * params.separation;
separationY = (separationY / separateMag) * params.separation;
}
// Combine all forces
this.vx += alignmentX + cohesionX + separationX;
this.vy += alignmentY + cohesionY + separationY;
}
}
draw(ctx) {
// Determine color based on mode
let color;
const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
switch(params.colorMode) {
case 'velocity':
color = `hsl(${(speed * 50) % 360}, 100%, 50%)`;
break;
case 'distance':
const avgDist = this.calculateAverageDistance();
color = `hsl(${(avgDist * 0.5) % 360}, 100%, 50%)`;
break;
case 'rainbow':
color = `hsl(${(this.baseHue + Date.now() * 0.01) % 360}, 100%, 50%)`;
break;
case 'pulse':
const pulseValue = Math.sin(this.pulsePhase) * 0.5 + 0.5;
color = `hsl(${this.baseHue}, 100%, ${pulseValue * 50 + 30}%)`;
break;
default:
color = this.color;
}
// 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++) {
const alpha = i / this.history.length * params.trailLength;
ctx.strokeStyle = color.replace(')', `, ${alpha})`).replace('hsl', 'hsla');
ctx.lineWidth = this.size * 0.7;
ctx.lineTo(this.history[i].x, this.history[i].y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(this.history[i].x, this.history[i].y);
}
}
// Draw particle
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
}
calculateAverageDistance() {
let totalDist = 0;
let count = 0;
for (let i = 1; i < this.history.length; i++) {
const dx = this.history[i].x - this.history[i-1].x;
const dy = this.history[i].y - this.history[i-1].y;
totalDist += Math.sqrt(dx * dx + dy * dy);
count++;
}
return count > 0 ? totalDist / count : 0;
}
}
// Simulation state
let particles = [];
let lastTime = 0;
let frameCount = 0;
let fps = 0;
function initSimulation() {
particles = [];
for (let i = 0; i < params.particleCount; i++) {
particles.push(new Particle());
}
}
function resetSimulation() {
initSimulation();
}
function animate(currentTime) {
// Calculate FPS
frameCount++;
if (currentTime - lastTime >= 1000) {
fps = frameCount;
frameCount = 0;
lastTime = currentTime;
}
// Clear canvas with fade effect
ctx.fillStyle = `rgba(0, 0, 0, ${params.trailLength})`;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Update and draw all particles
for (let particle of particles) {
particle.update(particles);
particle.draw(ctx);
}
// Draw FPS counter
ctx.fillStyle = 'white';
ctx.font = '14px Arial';
ctx.fillText(`FPS: ${fps}`, 10, 20);
ctx.fillText(`Particles: ${particles.length}`, 10, 40);
requestAnimationFrame(animate);
}
// Handle window resize
window.addEventListener('resize', () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
// Start simulation
initSimulation();
animate();
});
</script>
<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>
</html>