Spaces:
Running
Running
<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> |