diff --git a/README.md b/README.md index 2f8de6e..43edfe7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,59 @@ # Particle-Sim -a silly little particle simulator + +A simple 3D particle simulation to explore particles with interesting properties. + +## Features + +- **3D Particle System**: Real-time 3D particle simulation with proper depth perception +- **Physics Engine**: Gravity, attraction forces, and inter-particle interactions +- **Interactive Controls**: Adjust simulation parameters in real-time +- **Visual Effects**: + - Depth-based particle sizing and transparency + - Colorful particles with HSL color system + - Optional motion trails + - Glow effects +- **Rotation**: Automatic 3D rotation for better visualization +- **Performance Monitoring**: Real-time FPS and particle count display + +## How to Use + +1. Open `index.html` in a modern web browser +2. Use the control panel on the right to adjust simulation parameters: + - **Particle Count**: Number of particles (10-500) + - **Gravity**: Vertical force applied to particles (-1 to 1) + - **Attraction Force**: Force pulling particles to center (-2 to 2) + - **Particle Size**: Base size of particles (1-10) + - **Speed**: Simulation speed multiplier (0.1-3) + - **Rotation Speed**: Speed of 3D rotation (0-2) +3. Toggle effects: + - **Toggle Trails**: Show/hide particle motion trails + - **Toggle Depth Effect**: Enable/disable depth-based sizing + - **Reset Simulation**: Reset all particles to initial positions + +## Interesting Properties + +The particles exhibit several interesting behaviors: + +- **Gravitational Pull**: Particles are attracted to the center, creating swirling patterns +- **Inter-particle Forces**: Nearby particles interact with each other +- **3D Motion**: Particles move freely in 3D space with realistic physics +- **Color Variety**: Each particle has a unique color based on its initial position +- **Boundary Wrapping**: Particles wrap around when they reach the edges + +## Technical Details + +- Pure HTML5, CSS3, and JavaScript (no external dependencies) +- Canvas-based rendering with 3D to 2D projection +- Verlet integration for smooth physics simulation +- Optimized rendering with depth sorting for proper visual layering + +## Screenshots + +### Default View +![Default simulation](https://github.com/user-attachments/assets/a2673839-4dda-4bb4-9be7-a014acec13fa) + +### With Trails Enabled +![Trails enabled](https://github.com/user-attachments/assets/38da926b-2686-4afb-a364-07ef611d9c1e) + +### High Particle Count +![Many particles](https://github.com/user-attachments/assets/7273c8d8-991e-4289-8158-2f4f8c7260ce) diff --git a/index.html b/index.html new file mode 100644 index 0000000..9f58080 --- /dev/null +++ b/index.html @@ -0,0 +1,171 @@ + + + + + + 3D Particle Simulation + + + +
+
+ +
+
+

3D Particle Sim

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + + +
+
FPS: 0
+
Particles: 0
+
+
+
+ + + + diff --git a/simulation.js b/simulation.js new file mode 100644 index 0000000..82c4b29 --- /dev/null +++ b/simulation.js @@ -0,0 +1,351 @@ +// 3D Particle Simulation +class Particle { + constructor(x, y, z) { + this.x = x; + this.y = y; + this.z = z; + + this.vx = (Math.random() - 0.5) * 2; + this.vy = (Math.random() - 0.5) * 2; + this.vz = (Math.random() - 0.5) * 2; + + this.ax = 0; + this.ay = 0; + this.az = 0; + + // Color based on initial position + this.hue = Math.random() * 360; + this.saturation = 70 + Math.random() * 30; + this.lightness = 50 + Math.random() * 20; + + this.mass = 1; + this.trail = []; + this.maxTrailLength = 10; + } + + applyForce(fx, fy, fz) { + this.ax += fx / this.mass; + this.ay += fy / this.mass; + this.az += fz / this.mass; + } + + update(speedMultiplier) { + // Update velocity with acceleration + this.vx += this.ax * speedMultiplier; + this.vy += this.ay * speedMultiplier; + this.vz += this.az * speedMultiplier; + + // Apply damping + this.vx *= 0.99; + this.vy *= 0.99; + this.vz *= 0.99; + + // Store trail + if (config.showTrails) { + this.trail.push({ x: this.x, y: this.y, z: this.z }); + if (this.trail.length > this.maxTrailLength) { + this.trail.shift(); + } + } else { + this.trail = []; + } + + // Update position + this.x += this.vx * speedMultiplier; + this.y += this.vy * speedMultiplier; + this.z += this.vz * speedMultiplier; + + // Reset acceleration + this.ax = 0; + this.ay = 0; + this.az = 0; + + // Boundary wrapping + if (this.x > config.boundary) this.x = -config.boundary; + if (this.x < -config.boundary) this.x = config.boundary; + if (this.y > config.boundary) this.y = -config.boundary; + if (this.y < -config.boundary) this.y = config.boundary; + if (this.z > config.boundary) this.z = -config.boundary; + if (this.z < -config.boundary) this.z = config.boundary; + } +} + +// Configuration +const config = { + particleCount: 100, + gravity: 0.1, + attraction: 0.5, + particleSize: 3, + speed: 1.0, + rotationSpeed: 0.5, + showTrails: false, + showDepth: true, + fov: 500, + cameraZ: 1000, + boundary: 400 +}; + +// Canvas setup +const canvas = document.getElementById('canvas'); +const ctx = canvas.getContext('2d'); + +canvas.width = window.innerWidth - 300; +canvas.height = window.innerHeight; + +const centerX = canvas.width / 2; +const centerY = canvas.height / 2; + +// Particles array +let particles = []; + +// Rotation angles +let angleX = 0; +let angleY = 0; + +// Initialize particles +function initParticles() { + particles = []; + for (let i = 0; i < config.particleCount; i++) { + const x = (Math.random() - 0.5) * 400; + const y = (Math.random() - 0.5) * 400; + const z = (Math.random() - 0.5) * 400; + particles.push(new Particle(x, y, z)); + } +} + +// 3D to 2D projection +function project(x, y, z) { + const scale = config.fov / (config.fov + z + config.cameraZ); + const x2d = x * scale + centerX; + const y2d = y * scale + centerY; + return { x: x2d, y: y2d, scale }; +} + +// Rotate point around X axis +function rotateX(x, y, z, angle) { + const cos = Math.cos(angle); + const sin = Math.sin(angle); + return { + x: x, + y: y * cos - z * sin, + z: y * sin + z * cos + }; +} + +// Rotate point around Y axis +function rotateY(x, y, z, angle) { + const cos = Math.cos(angle); + const sin = Math.sin(angle); + return { + x: x * cos + z * sin, + y: y, + z: -x * sin + z * cos + }; +} + +// Apply forces to particles +function applyForces() { + particles.forEach(particle => { + // Gravity + particle.applyForce(0, config.gravity, 0); + + // Attraction to center + const dx = 0 - particle.x; + const dy = 0 - particle.y; + const dz = 0 - particle.z; + const distance = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (distance > 0) { + const force = config.attraction / (distance * distance + 1); + particle.applyForce(dx * force, dy * force, dz * force); + } + + // Inter-particle forces (only for nearby particles) + particles.forEach(other => { + if (particle !== other) { + const dx = other.x - particle.x; + const dy = other.y - particle.y; + const dz = other.z - particle.z; + const distance = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (distance < 50 && distance > 0) { + const force = 0.01 / (distance * distance + 1); + particle.applyForce(dx * force, dy * force, dz * force); + } + } + }); + }); +} + +// Update rotation +function updateRotation() { + angleY += config.rotationSpeed * 0.01; + angleX += config.rotationSpeed * 0.005; +} + +// Draw particles +function draw() { + // Clear canvas with trail effect + if (config.showTrails) { + ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } else { + ctx.fillStyle = 'rgba(0, 0, 0, 1)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + + // Sort particles by z-depth for proper rendering + const particlesToDraw = particles.map(particle => { + let pos = rotateX(particle.x, particle.y, particle.z, angleX); + pos = rotateY(pos.x, pos.y, pos.z, angleY); + const projected = project(pos.x, pos.y, pos.z); + return { + particle, + pos, + projected, + depth: pos.z + }; + }).sort((a, b) => a.depth - b.depth); + + // Draw trails + if (config.showTrails) { + particlesToDraw.forEach(({ particle, pos }) => { + if (particle.trail.length > 1) { + ctx.strokeStyle = `hsla(${particle.hue}, ${particle.saturation}%, ${particle.lightness}%, 0.3)`; + ctx.lineWidth = 1; + ctx.beginPath(); + + particle.trail.forEach((point, index) => { + let trailPos = rotateX(point.x, point.y, point.z, angleX); + trailPos = rotateY(trailPos.x, trailPos.y, trailPos.z, angleY); + const trailProjected = project(trailPos.x, trailPos.y, trailPos.z); + + if (index === 0) { + ctx.moveTo(trailProjected.x, trailProjected.y); + } else { + ctx.lineTo(trailProjected.x, trailProjected.y); + } + }); + + ctx.stroke(); + } + }); + } + + // Draw particles + particlesToDraw.forEach(({ particle, pos, projected, depth }) => { + let size = config.particleSize; + let alpha = 1; + + if (config.showDepth) { + // Size based on depth + size = config.particleSize * projected.scale * 2; + // Alpha based on depth + alpha = Math.max(0.2, Math.min(1, (depth + config.cameraZ) / (config.cameraZ * 1.5))); + } + + // Draw particle + ctx.beginPath(); + ctx.arc(projected.x, projected.y, size, 0, Math.PI * 2); + ctx.fillStyle = `hsla(${particle.hue}, ${particle.saturation}%, ${particle.lightness}%, ${alpha})`; + ctx.fill(); + + // Add glow effect + if (size > 2) { + ctx.beginPath(); + ctx.arc(projected.x, projected.y, size * 1.5, 0, Math.PI * 2); + ctx.fillStyle = `hsla(${particle.hue}, ${particle.saturation}%, ${particle.lightness}%, ${alpha * 0.2})`; + ctx.fill(); + } + }); +} + +// Animation loop +let lastTime = Date.now(); +let fps = 0; +let frameCount = 0; +let fpsUpdateTime = Date.now(); + +function animate() { + const currentTime = Date.now(); + const deltaTime = currentTime - lastTime; + lastTime = currentTime; + + // Update FPS + frameCount++; + if (currentTime - fpsUpdateTime > 1000) { + fps = frameCount; + frameCount = 0; + fpsUpdateTime = currentTime; + document.getElementById('fps').textContent = fps; + document.getElementById('particleStats').textContent = particles.length; + } + + // Update physics + applyForces(); + particles.forEach(particle => particle.update(config.speed)); + + // Update rotation + updateRotation(); + + // Draw + draw(); + + requestAnimationFrame(animate); +} + +// Control handlers +document.getElementById('particleCount').addEventListener('input', (e) => { + config.particleCount = parseInt(e.target.value); + document.getElementById('particleCountValue').textContent = config.particleCount; + initParticles(); +}); + +document.getElementById('gravity').addEventListener('input', (e) => { + config.gravity = parseFloat(e.target.value); + document.getElementById('gravityValue').textContent = config.gravity.toFixed(2); +}); + +document.getElementById('attraction').addEventListener('input', (e) => { + config.attraction = parseFloat(e.target.value); + document.getElementById('attractionValue').textContent = config.attraction.toFixed(1); +}); + +document.getElementById('particleSize').addEventListener('input', (e) => { + config.particleSize = parseFloat(e.target.value); + document.getElementById('particleSizeValue').textContent = config.particleSize.toFixed(1); +}); + +document.getElementById('speed').addEventListener('input', (e) => { + config.speed = parseFloat(e.target.value); + document.getElementById('speedValue').textContent = config.speed.toFixed(1); +}); + +document.getElementById('rotationSpeed').addEventListener('input', (e) => { + config.rotationSpeed = parseFloat(e.target.value); + document.getElementById('rotationSpeedValue').textContent = config.rotationSpeed.toFixed(1); +}); + +document.getElementById('reset').addEventListener('click', () => { + initParticles(); + angleX = 0; + angleY = 0; +}); + +document.getElementById('toggleTrails').addEventListener('click', () => { + config.showTrails = !config.showTrails; +}); + +document.getElementById('toggleDepth').addEventListener('click', () => { + config.showDepth = !config.showDepth; +}); + +// Handle window resize +window.addEventListener('resize', () => { + canvas.width = window.innerWidth - 300; + canvas.height = window.innerHeight; +}); + +// Start simulation +initParticles(); +animate();