claude-chamber / index.html
jbilcke-hf's picture
jbilcke-hf HF Staff
Upload 2 files
afcc7c7 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cloud Chamber Simulator</title>
<style>
body { margin: 0; overflow: hidden; background: #000; color: white; font-family: sans-serif; }
canvas { display: block; }
#rock-sprite {
position: absolute;
width: 80px;
height: 80px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
cursor: grab;
z-index: 10;
user-select: none;
}
#rock-sprite:active {
cursor: grabbing;
}
#gui {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 8px;
font-size: 14px;
}
#performance {
position: absolute;
top: 10px;
left: 10px;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 8px;
font-size: 14px;
font-family: monospace;
color: #0f0;
min-width: 120px;
}
#gui label {
display: block;
margin-top: 10px;
}
#gui input, #gui select {
width: 100%;
margin-top: 4px;
}
.slider-container {
display: flex;
align-items: center;
gap: 8px;
margin-top: 4px;
}
.slider-value {
font-size: 12px;
color: #ccc;
white-space: nowrap;
}
.slider-input {
flex: 1;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/gl-matrix-min.js"></script>
</head>
<body>
<canvas id="glcanvas"></canvas>
<img id="rock-sprite" src="rock-sprite.png" alt="Radiation Source">
<div id="performance">
<div>FPS: <span id="fps-counter">0</span></div>
<div>Particles: <span id="particle-counter">0</span></div>
</div>
<div id="gui">
<label>Background Cosmic Intensity
<div class="slider-container">
<span class="slider-value">0</span>
<input type="range" id="intensity" class="slider-input" min="0" max="100" step="0.1" value="0">
<span class="slider-value">100</span>
</div>
<div class="slider-value">Current: <span id="intensity-value">0</span></div>
</label>
<label>Radiation Source Intensity
<div class="slider-container">
<span class="slider-value">0</span>
<input type="range" id="sourceIntensity" class="slider-input" min="0" max="10" step="0.1" value="2">
<span class="slider-value">10</span>
</div>
<div class="slider-value">Current: <span id="sourceIntensity-value">2</span></div>
</label>
<label>Particle Size
<div class="slider-container">
<span class="slider-value">0.05</span>
<input type="range" id="size" class="slider-input" min="0.05" max="2.0" step="0.05" value="0.6">
<span class="slider-value">2.0</span>
</div>
<div class="slider-value">Current: <span id="size-value">0.6</span></div>
</label>
<label>Heat Flow
<div class="slider-container">
<span class="slider-value">0</span>
<input type="range" id="heatFlow" class="slider-input" min="0" max="0.5" step="0.01" value="0.04">
<span class="slider-value">0.5</span>
</div>
<div class="slider-value">Current: <span id="heatFlow-value">0.04</span></div>
</label>
<label>Trail Lifetime
<div class="slider-container">
<span class="slider-value">0.01</span>
<input type="range" id="trailLife" class="slider-input" min="0.01" max="1.0" step="0.01" value="0.03">
<span class="slider-value">1.0</span>
</div>
<div class="slider-value">Current: <span id="trailLife-value">0.03</span></div>
</label>
<label>Central Source Material
<select id="radiationType">
<option value="Uranium-238">Uranium-238 (Alpha)</option>
<option value="Cesium-137">Cesium-137 (Beta/Gamma)</option>
<option value="Cobalt-60">Cobalt-60 (Gamma)</option>
<option value="Radium-226">Radium-226 (Alpha)</option>
<option value="Strontium-90">Strontium-90 (Beta)</option>
<option value="Americium-241">Americium-241 (Alpha)</option>
</select>
</label>
<label>Cosmic Particle Type
<select id="cosmicType">
<option value="Muons">Muons</option>
<option value="Protons">Protons</option>
<option value="Electrons">Electrons</option>
<option value="Pions">Pions</option>
<option value="Mixed">Mixed Cosmic Ray</option>
</select>
</label>
<label>Lift Force
<div class="slider-container">
<span class="slider-value">0</span>
<input type="range" id="liftForce" class="slider-input" min="0" max="0.1" step="0.005" value="0.05">
<span class="slider-value">0.1</span>
</div>
<div class="slider-value">Current: <span id="liftForce-value">0.05</span></div>
</label>
<label>Friction Variability
<div class="slider-container">
<span class="slider-value">0</span>
<input type="range" id="frictionVariance" class="slider-input" min="0" max="1.0" step="0.005" value="0.4">
<span class="slider-value">1.0</span>
</div>
<div class="slider-value">Current: <span id="frictionVariance-value">0.4</span></div>
</label>
<label>Particle Curvature
<div class="slider-container">
<span class="slider-value">0</span>
<input type="range" id="curvatureMultiplier" class="slider-input" min="0" max="5.0" step="0.1" value="1.0">
<span class="slider-value">5.0</span>
</div>
<div class="slider-value">Current: <span id="curvatureMultiplier-value">1.0</span></div>
</label>
<label>Trail Length Scale
<div class="slider-container">
<span class="slider-value">1.0</span>
<input type="range" id="trailLengthScale" class="slider-input" min="1.0" max="10.0" step="0.1" value="2.0">
<span class="slider-value">10.0</span>
</div>
<div class="slider-value">Current: <span id="trailLengthScale-value">2.0</span></div>
</label>
</div>
<script>
const params = {
intensity: 0,
sourceIntensity: 2,
size: 0.6,
heatFlow: 0.04,
trailLife: 0.03,
trailLengthScale: 2.0,
radiationType: 'Cesium-137',
cosmicType: 'Muons',
liftForce: 0.05,
frictionVariance: 0.4,
curvatureMultiplier: 1.0
};
for (const id in params) {
const el = document.getElementById(id);
el.addEventListener('input', () => {
params[id] = el.type === 'range' ? parseFloat(el.value) : el.value;
if (el.type === 'range') {
const valueSpan = document.getElementById(id + '-value');
if (valueSpan) valueSpan.textContent = el.value;
}
});
}
// Sprite dragging functionality
const rockSprite = document.getElementById('rock-sprite');
let isDragging = false;
let dragOffset = { x: 0, y: 0 };
let spritePosition = { x: 0, y: 0 }; // Position in world coordinates
function screenToWorld(screenX, screenY) {
const canvas = document.getElementById("glcanvas");
const rect = canvas.getBoundingClientRect();
// Convert to normalized device coordinates (-1 to 1)
const ndcX = ((screenX - rect.left) / rect.width) * 2 - 1;
const ndcY = -(((screenY - rect.top) / rect.height) * 2 - 1);
// Calculate world space dimensions at z=0 plane
// Camera is at z=-50, FOV = PI/3, distance to z=0 = 50
const fov = Math.PI / 3;
const distance = 50;
const halfHeight = Math.tan(fov / 2) * distance;
const halfWidth = halfHeight * (canvas.width / canvas.height);
// Convert NDC to world coordinates
const worldX = ndcX * halfWidth;
const worldY = ndcY * halfHeight;
return { x: worldX, y: worldY };
}
function worldToScreen(worldX, worldY) {
const canvas = document.getElementById("glcanvas");
const rect = canvas.getBoundingClientRect();
// Calculate world space dimensions at z=0 plane
const fov = Math.PI / 3;
const distance = 50;
const halfHeight = Math.tan(fov / 2) * distance;
const halfWidth = halfHeight * (canvas.width / canvas.height);
// Convert world coordinates to NDC
const ndcX = worldX / halfWidth;
const ndcY = worldY / halfHeight;
// Convert NDC to screen coordinates
const screenX = (ndcX + 1) * 0.5 * rect.width + rect.left;
const screenY = (-ndcY + 1) * 0.5 * rect.height + rect.top;
return { x: screenX, y: screenY };
}
rockSprite.addEventListener('mousedown', (e) => {
isDragging = true;
const rect = rockSprite.getBoundingClientRect();
dragOffset.x = e.clientX - rect.left - rect.width / 2;
dragOffset.y = e.clientY - rect.top - rect.height / 2;
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
const newX = e.clientX - dragOffset.x;
const newY = e.clientY - dragOffset.y;
rockSprite.style.left = newX + 'px';
rockSprite.style.top = newY + 'px';
rockSprite.style.transform = 'translate(-50%, -50%)';
// Update world position
const worldPos = screenToWorld(newX, newY); // newX, newY is already the center due to CSS transform
spritePosition.x = worldPos.x;
spritePosition.y = worldPos.y;
}
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
const canvas = document.getElementById("glcanvas");
const gl = canvas.getContext("webgl");
if (!gl) {
alert("WebGL not supported");
}
// Create matrices early so they can be used in resize function
const projection = glMatrix.mat4.create();
const modelView = glMatrix.mat4.create();
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
// Update projection matrix with new aspect ratio
glMatrix.mat4.perspective(projection, Math.PI / 3, canvas.width / canvas.height, 0.1, 100);
// Update sprite screen position to maintain world coordinates
if (spritePosition.x !== 0 || spritePosition.y !== 0) {
const screenPos = worldToScreen(spritePosition.x, spritePosition.y);
rockSprite.style.left = screenPos.x + 'px';
rockSprite.style.top = screenPos.y + 'px';
}
}
window.addEventListener('resize', resize);
resize();
const vsSource = `
attribute vec3 aPosition;
attribute float aLife;
attribute vec3 aColor;
attribute float aDensity;
uniform float uPointSize;
uniform mat4 uProjection;
uniform mat4 uModelView;
varying float vLife;
varying vec3 vColor;
varying float vDensity;
void main() {
gl_Position = uProjection * uModelView * vec4(aPosition, 1.0);
gl_PointSize = uPointSize * (0.5 + vDensity);
vLife = aLife;
vColor = aColor;
vDensity = aDensity;
}
`;
const fsSource = `
precision mediump float;
varying float vLife;
varying vec3 vColor;
varying float vDensity;
void main() {
vec2 coord = gl_PointCoord - vec2(0.5);
float distance = length(coord);
float alpha = vLife * vDensity * (1.0 - smoothstep(0.0, 0.5, distance));
alpha *= exp(-distance * (2.0 + vDensity));
gl_FragColor = vec4(vColor, alpha);
}
`;
function compileShader(source, type) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error("Shader compile error:", gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
const vertexShader = compileShader(vsSource, gl.VERTEX_SHADER);
const fragmentShader = compileShader(fsSource, gl.FRAGMENT_SHADER);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
const aPosition = gl.getAttribLocation(program, "aPosition");
const aLife = gl.getAttribLocation(program, "aLife");
const aColor = gl.getAttribLocation(program, "aColor");
const aDensity = gl.getAttribLocation(program, "aDensity");
const uPointSize = gl.getUniformLocation(program, "uPointSize");
const uProjection = gl.getUniformLocation(program, "uProjection");
const uModelView = gl.getUniformLocation(program, "uModelView");
// Fixed particle pool system
const PARTICLE_POOL_SIZE = 40000;
const particles = [];
let activeParticleCount = 0;
// Initialize particle pool
for (let i = 0; i < PARTICLE_POOL_SIZE; i++) {
particles.push({
position: [0, 0, 0],
age: 0,
totalLife: 0,
friction: 1.0,
color: [1.0, 1.0, 1.0],
source: 'cosmic',
density: 0.5,
particleType: 'mixed',
active: false
});
}
// Performance tracking
let frameCount = 0;
let lastTime = performance.now();
let fps = 0;
const fpsCounter = document.getElementById('fps-counter');
const particleCounter = document.getElementById('particle-counter');
// Particle recycling
function getInactiveParticle() {
for (let i = 0; i < PARTICLE_POOL_SIZE; i++) {
if (!particles[i].active) {
return particles[i];
}
}
return null; // Pool is full
}
function activateParticle(particle, position, totalLife, friction, color, source, density, particleType) {
particle.position[0] = position[0];
particle.position[1] = position[1];
particle.position[2] = position[2];
particle.age = 0;
particle.totalLife = totalLife;
particle.friction = friction;
particle.color = color;
particle.source = source;
particle.density = density;
particle.particleType = particleType;
particle.active = true;
activeParticleCount++;
}
function deactivateParticle(particle) {
if (particle.active) {
particle.active = false;
activeParticleCount--;
}
}
function getRadiationProperties(material) {
const properties = {
'Uranium-238': {
length: [15, 25], spacing: 0.08, color: [1.0, 0.8, 0.6],
density: 0.9, curvature: 0.02, scattering: 0.1, type: 'alpha'
},
'Cesium-137': {
length: [20, 40], spacing: 0.06, color: [0.8, 1.0, 0.8],
density: 0.6, curvature: 0.05, scattering: 0.3, type: 'beta-gamma'
},
'Cobalt-60': {
length: [25, 50], spacing: 0.04, color: [0.6, 0.8, 1.0],
density: 0.3, curvature: 0.001, scattering: 0.05, type: 'gamma'
},
'Radium-226': {
length: [12, 20], spacing: 0.1, color: [1.0, 0.6, 0.8],
density: 0.95, curvature: 0.015, scattering: 0.08, type: 'alpha'
},
'Strontium-90': {
length: [30, 45], spacing: 0.05, color: [1.0, 1.0, 0.6],
density: 0.5, curvature: 0.08, scattering: 0.4, type: 'beta'
},
'Americium-241': {
length: [10, 18], spacing: 0.12, color: [0.8, 0.6, 1.0],
density: 0.85, curvature: 0.025, scattering: 0.12, type: 'alpha'
}
};
return properties[material] || properties['Uranium-238'];
}
function getCosmicProperties(type) {
const properties = {
'Muons': {
length: [200, 300], spacing: 0.03, penetration: 0.9,
density: 0.2, curvature: 0.001, scattering: 0.02, type: 'muon'
},
'Protons': {
length: [150, 250], spacing: 0.05, penetration: 0.7,
density: 0.6, curvature: 0.03, scattering: 0.15, type: 'proton'
},
'Electrons': {
length: [100, 180], spacing: 0.08, penetration: 0.3,
density: 0.15, curvature: 0.12, scattering: 0.6, type: 'electron'
},
'Pions': {
length: [180, 280], spacing: 0.06, penetration: 0.6,
density: 0.4, curvature: 0.04, scattering: 0.25, type: 'pion'
},
'Mixed': {
length: [150, 300], spacing: 0.04, penetration: 0.8,
density: 0.3, curvature: 0.06, scattering: 0.3, type: 'mixed'
}
};
return properties[type] || properties['Muons'];
}
function spawnCentralSourceTrail() {
const props = getRadiationProperties(params.radiationType);
const baseLength = props.length[0] + Math.floor(Math.random() * (props.length[1] - props.length[0]));
const length = Math.floor(baseLength * 5 * params.trailLengthScale * props.density);
let dir = [Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5];
const mag = Math.sqrt(dir[0]**2 + dir[1]**2 + dir[2]**2);
dir[0] /= mag; dir[1] /= mag; dir[2] /= mag;
let currentPos = [spritePosition.x, spritePosition.y, 0];
let currentDir = [...dir];
for (let i = 0; i < length; i++) {
if (Math.random() < props.scattering && i > 5) {
const scatterAngle = (Math.random() - 0.5) * props.scattering * 2;
const perpDir = [
Math.random() - 0.5,
Math.random() - 0.5,
Math.random() - 0.5
];
currentDir[0] += perpDir[0] * scatterAngle;
currentDir[1] += perpDir[1] * scatterAngle;
currentDir[2] += perpDir[2] * scatterAngle;
const newMag = Math.sqrt(currentDir[0]**2 + currentDir[1]**2 + currentDir[2]**2);
currentDir[0] /= newMag; currentDir[1] /= newMag; currentDir[2] /= newMag;
}
const curveFactor = props.curvature * params.curvatureMultiplier * Math.sin(i * 0.1) * (1 + Math.random() * 0.5);
currentDir[0] += curveFactor * (Math.random() - 0.5);
currentDir[1] += curveFactor * (Math.random() - 0.5);
currentDir[2] += curveFactor * (Math.random() - 0.5);
const dirMag = Math.sqrt(currentDir[0]**2 + currentDir[1]**2 + currentDir[2]**2);
currentDir[0] /= dirMag; currentDir[1] /= dirMag; currentDir[2] /= dirMag;
currentPos[0] += currentDir[0] * props.spacing;
currentPos[1] += currentDir[1] * props.spacing;
currentPos[2] += currentDir[2] * props.spacing;
const particle = getInactiveParticle();
if (particle) {
activateParticle(
particle,
[...currentPos],
params.trailLife + 1.0,
1.0 - Math.random() * params.frictionVariance,
props.color,
'central',
props.density,
props.type
);
}
}
}
function spawnCosmicTrail() {
const props = getCosmicProperties(params.cosmicType);
const baseLength = props.length[0] + Math.floor(Math.random() * (props.length[1] - props.length[0]));
const length = Math.floor(baseLength * 3 * params.trailLengthScale * props.density);
// Generate completely random direction for cosmic particles
const angle1 = Math.random() * Math.PI * 2;
const angle2 = Math.random() * Math.PI;
let currentDir = [
Math.sin(angle2) * Math.cos(angle1),
Math.sin(angle2) * Math.sin(angle1),
Math.cos(angle2)
];
// Start from a random position on the edge of the simulation space
const startDistance = 60 + Math.random() * 20;
const startAngle1 = Math.random() * Math.PI * 2;
const startAngle2 = Math.random() * Math.PI;
let currentPos = [
Math.sin(startAngle2) * Math.cos(startAngle1) * startDistance,
Math.sin(startAngle2) * Math.sin(startAngle1) * startDistance,
Math.cos(startAngle2) * startDistance
];
for (let i = 0; i < length; i++) {
if (Math.random() < props.scattering && i > 10) {
const scatterAngle = (Math.random() - 0.5) * props.scattering;
const perpDir = [
Math.random() - 0.5,
Math.random() - 0.5,
Math.random() - 0.5
];
currentDir[0] += perpDir[0] * scatterAngle;
currentDir[1] += perpDir[1] * scatterAngle;
currentDir[2] += perpDir[2] * scatterAngle;
const newMag = Math.sqrt(currentDir[0]**2 + currentDir[1]**2 + currentDir[2]**2);
currentDir[0] /= newMag; currentDir[1] /= newMag; currentDir[2] /= newMag;
}
if (props.type === 'electron' || props.type === 'mixed') {
const tangleFactor = props.curvature * params.curvatureMultiplier * (1 + Math.random());
currentDir[0] += tangleFactor * (Math.random() - 0.5);
currentDir[1] += tangleFactor * (Math.random() - 0.5);
currentDir[2] += tangleFactor * (Math.random() - 0.5);
const dirMag = Math.sqrt(currentDir[0]**2 + currentDir[1]**2 + currentDir[2]**2);
currentDir[0] /= dirMag; currentDir[1] /= dirMag; currentDir[2] /= dirMag;
}
currentPos[0] += currentDir[0] * props.spacing;
currentPos[1] += currentDir[1] * props.spacing;
currentPos[2] += currentDir[2] * props.spacing;
const particle = getInactiveParticle();
if (particle) {
activateParticle(
particle,
[...currentPos],
(params.trailLife + 1.0) * props.penetration,
1.0 - Math.random() * params.frictionVariance * 0.5,
[1.0, 1.0, 1.0],
'cosmic',
props.density,
props.type
);
}
}
}
// Set up model view matrix (projection is handled in resize function)
glMatrix.mat4.translate(modelView, modelView, [0, 0, -50]);
const positionBuffer = gl.createBuffer();
const lifeBuffer = gl.createBuffer();
const colorBuffer = gl.createBuffer();
const densityBuffer = gl.createBuffer();
function render() {
// FPS calculation
frameCount++;
const currentTime = performance.now();
if (currentTime - lastTime >= 1000) {
fps = Math.round((frameCount * 1000) / (currentTime - lastTime));
frameCount = 0;
lastTime = currentTime;
fpsCounter.textContent = fps;
}
// Update particle count
particleCounter.textContent = activeParticleCount;
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
if (Math.random() < 0.1 * params.intensity) {
spawnCosmicTrail();
}
if (Math.random() < 0.1 * params.sourceIntensity) {
spawnCentralSourceTrail();
}
// Update and deactivate particles
for (let i = 0; i < PARTICLE_POOL_SIZE; i++) {
const p = particles[i];
if (!p.active) continue;
p.age += 0.01 * p.friction;
// Apply physics if particle is still in visible lifetime
if (p.age <= params.trailLife + 1.0) {
const f = params.heatFlow;
p.position[0] += (Math.random() - 0.5) * f * p.friction;
p.position[1] += (Math.random() - 0.5) * f * p.friction + params.liftForce * p.friction;
p.position[2] += (Math.random() - 0.5) * f * p.friction;
}
// Deactivate particles that have exceeded their lifetime or are out of bounds
if (p.age > p.totalLife ||
Math.abs(p.position[0]) > 100 ||
Math.abs(p.position[1]) > 100 ||
Math.abs(p.position[2]) > 100) {
deactivateParticle(p);
}
}
// Build arrays only for active particles
const posArray = new Float32Array(activeParticleCount * 3);
const lifeArray = new Float32Array(activeParticleCount);
const colorArray = new Float32Array(activeParticleCount * 3);
const densityArray = new Float32Array(activeParticleCount);
let activeIndex = 0;
for (let i = 0; i < PARTICLE_POOL_SIZE; i++) {
const p = particles[i];
if (!p.active) continue;
posArray.set(p.position, activeIndex * 3);
let alpha = 1.0;
const fadeStart = params.trailLife * 0.7;
if (p.age < fadeStart) {
alpha = 1.0;
} else if (p.age < p.totalLife) {
const fadeProgress = (p.age - fadeStart) / (p.totalLife - fadeStart);
alpha = 1.0 - Math.pow(fadeProgress, 0.8);
} else {
alpha = 0.0;
}
lifeArray[activeIndex] = Math.max(0.0, alpha);
colorArray.set(p.color || [1.0, 1.0, 1.0], activeIndex * 3);
densityArray[activeIndex] = p.density || 0.5;
activeIndex++;
}
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, posArray, gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, lifeBuffer);
gl.bufferData(gl.ARRAY_BUFFER, lifeArray, gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(aLife);
gl.vertexAttribPointer(aLife, 1, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colorArray, gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(aColor);
gl.vertexAttribPointer(aColor, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, densityBuffer);
gl.bufferData(gl.ARRAY_BUFFER, densityArray, gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(aDensity);
gl.vertexAttribPointer(aDensity, 1, gl.FLOAT, false, 0, 0);
gl.uniformMatrix4fv(uProjection, false, projection);
gl.uniformMatrix4fv(uModelView, false, modelView);
gl.uniform1f(uPointSize, 10.0 * params.size);
gl.drawArrays(gl.POINTS, 0, activeParticleCount);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
</script>
</body>
</html>