anycoder-9ae2c0a2 / index.html
andorxotnot's picture
Deploy from anycoder
55136bb verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Continuous Particle Life</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;500;700&display=swap" rel="stylesheet">
<style>
:root {
--primary: #00f2ff;
--glass-bg: rgba(15, 23, 42, 0.6);
--glass-border: rgba(255, 255, 255, 0.1);
--text: #e2e8f0;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
}
body {
overflow: hidden;
background-color: #050505;
font-family: 'Inter', sans-serif;
color: var(--text);
}
/* Canvas */
#canvas-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
/* Header Link */
.brand-link {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 100;
color: var(--primary);
text-decoration: none;
font-weight: 700;
font-size: 0.9rem;
background: var(--glass-bg);
padding: 8px 16px;
border-radius: 20px;
border: 1px solid var(--glass-border);
backdrop-filter: blur(10px);
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 1px;
box-shadow: 0 4px 15px rgba(0, 242, 255, 0.1);
}
.brand-link:hover {
background: rgba(0, 242, 255, 0.1);
box-shadow: 0 4px 25px rgba(0, 242, 255, 0.3);
transform: translateX(-50%) translateY(-2px);
}
/* UI Overlay */
.ui-panel {
position: fixed;
top: 20px;
right: 20px;
width: 300px;
background: var(--glass-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--glass-border);
border-radius: 16px;
padding: 20px;
z-index: 10;
display: flex;
flex-direction: column;
gap: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
transition: transform 0.3s ease, opacity 0.3s ease;
max-height: 90vh;
overflow-y: auto;
}
.ui-panel.hidden {
transform: translateX(120%);
opacity: 0;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
h1 {
font-size: 1.1rem;
font-weight: 700;
background: linear-gradient(90deg, #fff, #94a3b8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.control-group {
display: flex;
flex-direction: column;
gap: 8px;
}
label {
font-size: 0.8rem;
color: #94a3b8;
display: flex;
justify-content: space-between;
}
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 4px;
background: rgba(255,255,255,0.1);
border-radius: 2px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
transition: transform 0.1s;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
button {
background: rgba(255,255,255,0.05);
border: 1px solid var(--glass-border);
color: white;
padding: 10px;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
font-family: 'Inter', sans-serif;
}
button:hover {
background: var(--primary);
color: black;
border-color: var(--primary);
}
button.secondary {
background: transparent;
border: 1px solid rgba(255,255,255,0.2);
}
button.secondary:hover {
background: rgba(255,255,255,0.1);
color: white;
}
.toggle-btn {
position: fixed;
top: 20px;
right: 20px;
z-index: 11;
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--glass-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
color: white;
display: none; /* Shown via JS logic if needed, or just use CSS media queries */
align-items: center;
justify-content: center;
cursor: pointer;
}
.stats {
font-size: 0.7rem;
color: #64748b;
margin-top: 10px;
text-align: center;
}
/* Scrollbar */
.ui-panel::-webkit-scrollbar {
width: 4px;
}
.ui-panel::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.2);
border-radius: 4px;
}
@media (max-width: 600px) {
.ui-panel {
width: calc(100% - 40px);
bottom: 20px;
top: auto;
max-height: 50vh;
}
.brand-link {
top: 10px;
font-size: 0.75rem;
}
}
</style>
<!-- Three.js and dependencies -->
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
</head>
<body>
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="brand-link">Built with anycoder</a>
<div id="canvas-container"></div>
<div class="ui-panel" id="uiPanel">
<div class="panel-header">
<h1>Particle Life 3D</h1>
</div>
<div class="control-group">
<label>Particles <span id="val-count">1000</span></label>
<input type="range" id="inp-count" min="200" max="2000" step="100" value="1000">
</div>
<div class="control-group">
<label>Interaction Radius <span id="val-radius">30</span></label>
<input type="range" id="inp-radius" min="10" max="100" value="30">
</div>
<div class="control-group">
<label>Force Strength <span id="val-force">1.0</span></label>
<input type="range" id="inp-force" min="0.1" max="5.0" step="0.1" value="1.0">
</div>
<div class="control-group">
<label>Friction <span id="val-friction">0.85</span></label>
<input type="range" id="inp-friction" min="0.50" max="0.99" step="0.01" value="0.85">
</div>
<button id="btn-randomize">🎲 Randomize Rules</button>
<button id="btn-reset" class="secondary">Example: Cells</button>
<button id="btn-reset-snake" class="secondary">Example: Serpents</button>
<div class="stats" id="stats">
FPS: 60 | Types: 4
</div>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
// --- Configuration ---
const CONFIG = {
particleCount: 1000,
types: 4,
radius: 30,
forceFactor: 1.0,
friction: 0.85,
worldSize: 200,
wrap: true
};
// --- State ---
let particles = [];
let rules = []; // Interaction matrix
let typeColors = [];
// --- Three.js Setup ---
const container = document.getElementById('canvas-container');
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x050505, 0.002);
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 100, 250);
const renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: "high-performance" });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // Cap pixel ratio for performance
container.appendChild(renderer.domElement);
// Post-processing (Bloom)
const renderScene = new RenderPass(scene, camera);
const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);
bloomPass.threshold = 0.1;
bloomPass.strength = 1.2; // Glowing effect
bloomPass.radius = 0.5;
const composer = new EffectComposer(renderer);
composer.addPass(renderScene);
composer.addPass(bloomPass);
// Controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.5;
// Lighting
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(10, 10, 10);
scene.add(dirLight);
// --- Particle System (InstancedMesh) ---
const geometry = new THREE.SphereGeometry(1, 16, 16);
const material = new THREE.MeshStandardMaterial({
color: 0xffffff,
roughness: 0.2,
metalness: 0.8,
emissive: 0xffffff,
emissiveIntensity: 0.2
});
let instancedMesh;
const dummy = new THREE.Object3D();
const _color = new THREE.Color();
// --- Logic ---
function initSystem() {
// Clear old mesh
if (instancedMesh) {
scene.remove(instancedMesh);
instancedMesh.dispose();
}
// Create Colors
typeColors = [
new THREE.Color(0x00f2ff), // Cyan
new THREE.Color(0xff0055), // Magenta
new THREE.Color(0x00ffaa), // Green
new THREE.Color(0xffaa00) // Orange
];
CONFIG.types = typeColors.length;
// Create Particles
particles = new Float32Array(CONFIG.particleCount * 8); // x, y, z, vx, vy, vz, type, size
for (let i = 0; i < CONFIG.particleCount; i++) {
const i8 = i * 8;
// Position
particles[i8] = (Math.random() - 0.5) * CONFIG.worldSize;
particles[i8+1] = (Math.random() - 0.5) * CONFIG.worldSize;
particles[i8+2] = (Math.random() - 0.5) * CONFIG.worldSize;
// Velocity
particles[i8+3] = 0;
particles[i8+4] = 0;
particles[i8+5] = 0;
// Type
particles[i8+6] = Math.floor(Math.random() * CONFIG.types);
// Size (base)
particles[i8+7] = 1.0 + Math.random() * 1.5;
}
// Create Instanced Mesh
instancedMesh = new THREE.InstancedMesh(geometry, material, CONFIG.particleCount);
instancedMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
scene.add(instancedMesh);
randomizeRules();
}
function randomizeRules() {
// Generate interaction matrix: force between type A and B
rules = [];
for (let i = 0; i < CONFIG.types; i++) {
rules[i] = [];
for (let j = 0; j < CONFIG.types; j++) {
// Value between -1 (repel) and 1 (attract)
rules[i][j] = (Math.random() * 2 - 1);
}
}
console.log("Rules Randomized", rules);
}
function setPreset(name) {
// Presets for interesting behaviors
if (name === 'cells') {
// Similar types attract, different repel slightly
for(let i=0; i<CONFIG.types; i++) {
for(let j=0; j<CONFIG.types; j++) {
if (i === j) rules[i][j] = 0.8;
else rules[i][j] = -0.4;
}
}
} else if (name === 'snakes') {
// Cyclic attraction: 0->1, 1->2, 2->3, 3->0
for(let i=0; i<CONFIG.types; i++) {
for(let j=0; j<CONFIG.types; j++) {
rules[i][j] = 0;
}
rules[i][(i+1)%CONFIG.types] = 0.6; // Chase next
rules[i][(i-1+CONFIG.types)%CONFIG.types] = -0.2; // Run from prev
}
}
}
// --- Physics Loop ---
function updatePhysics() {
const count = CONFIG.particleCount;
const rMax = CONFIG.radius;
const rMaxSq = rMax * rMax;
const forceFactor = CONFIG.forceFactor;
const friction = CONFIG.friction;
const worldSize = CONFIG.worldSize;
const halfWorld = worldSize / 2;
// Brute force O(N^2) - acceptable for N < 1500 in JS on modern devices
// Optimized slightly by pre-calculating constants
for (let i = 0; i < count; i++) {
const i8 = i * 8;
let fx = 0, fy = 0, fz = 0;
const typeI = particles[i8+6];
const px = particles[i8];
const py = particles[i8+1];
const pz = particles[i8+2];
for (let j = 0; j < count; j++) {
if (i === j) continue;
const j8 = j * 8;
let dx = particles[j8] - px;
let dy = particles[j8+1] - py;
let dz = particles[j8+2] - pz;
// Wrap around distance for continuous field illusion
if (dx > halfWorld) dx -= worldSize;
if (dx < -halfWorld) dx += worldSize;
if (dy > halfWorld) dy -= worldSize;
if (dy < -halfWorld) dy += worldSize;
if (dz > halfWorld) dz -= worldSize;
if (dz < -halfWorld) dz += worldSize;
const distSq = dx*dx + dy*dy + dz*dz;
if (distSq > 0 && distSq < rMaxSq) {
const dist = Math.sqrt(distSq);
const q = dist / rMax; // Normalized distance 0..1
const typeJ = particles[j8+6];
// Force calculation
// 1. Repulsion if very close (prevent overlap)
// 2. Interaction based on rule matrix
let f = 0;
if (q < 0.3) {
// Strong repulsion
f = q - 1;
} else {
// Rule based force.
// Smooth curve: rises then falls off
// Standard Particle Life formula variant
const g = rules[typeI][typeJ];
// Peak force at q=0.5 approx
f = g * (1 - Math.abs(2 * q - 1)) * 0.5;
}
// Normalize force vector
const fScaled = (f * forceFactor) / dist;
fx += dx * fScaled;
fy += dy * fScaled;
fz += dz * fScaled;
}
}
// Apply Force to Velocity
particles[i8+3] = (particles[i8+3] + fx) * friction;
particles[i8+4] = (particles[i8+4] + fy) * friction;
particles[i8+5] = (particles[i8+5] + fz) * friction;
// Update Position
particles[i8] += particles[i8+3];
particles[i8+1] += particles[i8+4];
particles[i8+2] += particles[i8+5];
// Boundary Wrap
if (particles[i8] <= -halfWorld) particles[i8] += worldSize;
if (particles[i8] >= halfWorld) particles[i8] -= worldSize;
if (particles[i8+1] <= -halfWorld) particles[i8+1] += worldSize;
if (particles[i8+1] >= halfWorld) particles[i8+1] -= worldSize;
if (particles[i8+2] <= -halfWorld) particles[i8+2] += worldSize;
if (particles[i8+2] >= halfWorld) particles[i8+2] -= worldSize;
}
}
function updateVisuals() {
const count = CONFIG.particleCount;
for (let i = 0; i < count; i++) {
const i8 = i * 8;
dummy.position.set(
particles[i8],
particles[i8+1],
particles[i8+2]
);
// Dynamic Scale based on velocity (stretch effect)
const speed = Math.sqrt(particles[i8+3]**2 + particles[i8+4]**2 + particles[i8+5]**2);
const baseSize = particles[i8+7];
const scale = baseSize + speed * 2;
// Orient towards velocity for "looking alive"
// Not strictly necessary for spheres but helps if we change geometry later
// dummy.lookAt(dummy.position.clone().add(new THREE.Vector3(particles[i8+3], particles[i8+4], particles[i8+5])));
dummy.scale.set(scale, scale, scale);
dummy.updateMatrix();
instancedMesh.setMatrixAt(i, dummy.matrix);
// Color
const type = particles[i8+6];
_color.copy(typeColors[type]);
// Brighten based on speed (activity)
const intensity = 1 + speed * 0.5;
_color.multiplyScalar(intensity);
instancedMesh.setColorAt(i, _color);
}
instancedMesh.instanceMatrix.needsUpdate = true;
if(instancedMesh.instanceColor) instancedMesh.instanceColor.needsUpdate = true;
}
// --- Main Loop ---
const statsEl = document.getElementById('stats');
let lastTime = 0;
let frames = 0;
let fpsTime = 0;
function animate(time) {
requestAnimationFrame(animate);
// FPS Calculation
frames++;
if (time - fpsTime > 1000) {
statsEl.innerHTML = `FPS: ${frames} | Particles: ${CONFIG.particleCount}`;
frames = 0;
fpsTime = time;
}
updatePhysics();
updateVisuals();
controls.update();
// Render with Bloom
composer.render();
}
// --- UI Event Listeners ---
document.getElementById('inp-count').addEventListener('input', (e) => {
CONFIG.particleCount = parseInt(e.target.value);
document.getElementById('val-count').innerText = CONFIG.particleCount;
initSystem();
});
document.getElementById('inp-radius').addEventListener('input', (e) => {
CONFIG.radius = parseInt(e.target.value);
document.getElementById('val-radius').innerText = CONFIG.radius;
});
document.getElementById('inp-force').addEventListener('input', (e) => {
CONFIG.forceFactor = parseFloat(e.target.value);
document.getElementById('val-force').innerText = CONFIG.forceFactor;
});
document.getElementById('inp-friction').addEventListener('input', (e) => {
CONFIG.friction = parseFloat(e.target.value);
document.getElementById('val-friction').innerText = CONFIG.friction;
});
document.getElementById('btn-randomize').addEventListener('click', () => {
randomizeRules();
// Add a flash effect to canvas maybe?
});
document.getElementById('btn-reset').addEventListener('click', () => {
setPreset('cells');
// Reset Friction/Force for this preset
CONFIG.friction = 0.80;
CONFIG.forceFactor = 1.2;
CONFIG.radius = 40;
updateUiValues();
});
document.getElementById('btn-reset-snake').addEventListener('click', () => {
setPreset('snakes');
CONFIG.friction = 0.90;
CONFIG.forceFactor = 2.0;
CONFIG.radius = 60;
updateUiValues();
});
function updateUiValues() {
document.getElementById('inp-friction').value = CONFIG.friction;
document.getElementById('val-friction').innerText = CONFIG.friction;
document.getElementById('inp-force').value = CONFIG.forceFactor;
document.getElementById('val-force').innerText = CONFIG.forceFactor;
document.getElementById('inp-radius').value = CONFIG.radius;
document.getElementById('val-radius').innerText = CONFIG.radius;
}
// Resize Handler
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
});
// --- Boot ---
initSystem();
animate();
</script>
</body>
</html>