123

Pen Settings

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

You're using npm packages, so we've auto-selected Babel for you here, which we require to process imports and make it all work. If you need to use a different JavaScript preprocessor, remove the packages in the npm tab.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Use npm Packages

We can make npm packages available for you to use in your JavaScript. We use webpack to prepare them and make them available to import. We'll also process your JavaScript with Babel.

⚠️ This feature can only be used by logged in users.

Code Indentation

     

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

HTML Settings

Here you can Sed posuere consectetur est at lobortis. Donec ullamcorper nulla non metus auctor fringilla. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec id elit non mi porta gravida at eget metus. Praesent commodo cursus magna, vel scelerisque nisl consectetur et.

            
              
<canvas></canvas>
<div class="info">
	<span id="fps">fps: 0</span>
	<span id="mspf">mspf: 0</span>
	<span>space to perturb</span>
	<span>drag droplets to manipulate</span>
</div>
<script type="text/x-vertex-shader" id="pass-through-vs">
    precision mediump float; // could use lowp...
    attribute vec2 aPosition;
    varying vec2 vTexcoord;
    
    void main() {
    	vTexcoord  = aPosition * 0.5 + vec2(0.5);
    	vTexcoord.y = 1.0 - vTexcoord.y;
    	gl_Position = vec4(aPosition, 0.0, 1.0);
    }

</script>
<script type="text/x-fragment-shader" id="alpha-mask-fs">
    precision mediump float;
    varying vec2 vTexcoord;
    uniform sampler2D uTexture;
    
    void main() {
    	vec4 color = texture2D(uTexture, vTexcoord);
    	//gl_FragColor = color;
    	float alpha = clamp(color.a*15.0-7.0, 0.0, 1.0);
    	gl_FragColor = vec4(0.2, 0.6, 1.0, alpha);
    }
</script>

<script type="text/x-vertex-shader" id="particle-vs">
    precision mediump float;
    
    attribute vec2 aPosition;
    attribute vec2 aTexcoord;
    
    varying vec2 vTexcoord;
    
    void main() {
    	vTexcoord = aTexcoord;
    	vec2 pos = aPosition;
    	pos = pos*2.0 - vec2(1.0);
    	gl_Position = vec4(pos.x, pos.y, 0.0, 1.0);
    }
</script>

<script type="text/x-fragment-shader" id="particle-fs">
    precision mediump float;
    
    varying vec2 vTexcoord;
    
    uniform sampler2D uTexture;
    
    void main() {
    	vec4 texColor = texture2D(uTexture, vTexcoord);
    	//float alpha = clamp(texColor.a*21.0-9.0, 0.0, 1.0);
    	gl_FragColor = texColor; //vec4(texColor.a);
    }
</script>
            
          
!
            
              canvas, html, body {
	width: 100%;
	height: 100%;
	overflow: hidden;
	margin: 0;
	padding: 0;
}

.info {
	position: absolute;
	top: 0;
	left: 0;
	width: 200px;
	font-family: monospace;
	color: #f80;
	span {
		display: block;
	}
	padding: 5px;
}

            
          
!
            
              (function() {
'use strict'
// these are tweakable. 
const NUM_PARTICLES = 800; // ideally would be fn of resolution...
const PHYSICS_ITERATIONS = 3; // reduce for faster/less accurate sim
const ELASTICITY = 0.97;
const X_ACCEL = 0.0;
const Y_ACCEL = 0.01; // gravity
const TIME_SCALE = 2; // lower == slow motion

const mainCanvas = document.getElementsByTagName('canvas')[0];

const gl = mainCanvas.getContext('webgl');
	  /*WebGLDebugUtils.makeDebugContext(mainCanvas.getContext('webgl'), function(err, funcName, args) {
	document.body.innerHTML = WebGLDebugUtils.glEnumToString(err) + " was caused by call to: " + funcName;
});*/

let DPI = Math.max(2, Math.min(1, Math.ceil(window.devicePixelRatio || window.webkitDevicePixelRatio || 1.0)));

let WIDTH = window.innerWidth;
let HEIGHT = window.innerHeight;

mainCanvas.width = WIDTH*DPI;
mainCanvas.height = HEIGHT*DPI;

const MOUSE = {
	isDown: false,
	transitions: 0,
	x: 0,
	y: 0,
};

const now = window.performance ? (() => performance.now()) : Date.now;

function updateMousePos({clientX, clientY}) {
	const {left, top} = mainCanvas.getBoundingClientRect();
	MOUSE.x = clientX-left;
	MOUSE.y = clientY-top;
}

window.addEventListener('mousedown', e => {
	MOUSE.isDown = true;
	++MOUSE.transitions;
	updateMousePos(e);
});

window.addEventListener('mouseup', e => {
	MOUSE.isDown = false;
	++MOUSE.transitions;
	updateMousePos(e);
});

window.addEventListener('mousemove', updateMousePos);

	
window.addEventListener('touchstart', e => {
	 e.preventDefault(); 
	MOUSE.isDown = true;
	++MOUSE.transitions;
	updateMousePos(e.touches[0]);
});

window.addEventListener('touchend', e => {
	MOUSE.isDown = false;
	++MOUSE.transitions;
	updateMousePos(e.touches[0]);
});

window.addEventListener('touchmove', e => { e.preventDefault(); updateMousePos(e.touches[0]) });

window.addEventListener('blur', () => {
	MOUSE.isDown = false;
	MOUSE.transitions = 0;
});

function compileShader(fragSrcOrId, vertSrcOrId, attribs={}) {
    let fragSrc = fragSrcOrId;
    if (fragSrcOrId.indexOf(' ') < 0) {
        let fragSrcElem = document.getElementById(fragSrcOrId);
        if (fragSrcElem) {
            fragSrc = fragSrcElem.textContent;
        }
    }
	const shader = gl.createProgram();
	const fs = gl.createShader(gl.FRAGMENT_SHADER);
	gl.shaderSource(fs, fragSrc);
	gl.compileShader(fs);
	if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) {
		console.error("Failed to compile fs: "+gl.getShaderInfoLog(fs));
	}
    let vertSrc = vertSrcOrId;
    if (fragSrcOrId.indexOf(' ') < 0) {
        let vertSrcElem = document.getElementById(vertSrcOrId);
        if (vertSrcElem) {
            vertSrc = vertSrcElem.textContent;
        }
    }
	const vs = gl.createShader(gl.VERTEX_SHADER);
	gl.shaderSource(vs, vertSrc);
	gl.compileShader(vs);
	if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) {
		console.error("Failed to compile vs: "+gl.getShaderInfoLog(vs));
	}

	gl.attachShader(shader, vs);
	gl.attachShader(shader, fs);
	Object.keys(attribs).forEach(k => 
		gl.bindAttribLocation(shader, attribs[k], k))

	gl.linkProgram(shader);
	gl.deleteShader(vs);
	gl.deleteShader(fs);
	if (!gl.getProgramParameter(shader, gl.LINK_STATUS)) {
		const log = gl.getProgramInfoLog(shader);
		console.error("shader link failed: program info log:\n"+log);
		throw Error("shader link failed");
	}
	const uniforms = {};
	const len = gl.getProgramParameter(shader, gl.ACTIVE_UNIFORMS) || 0;
	for (let i = 0; i < len; ++i) {
		const info = gl.getActiveUniform(shader, i);
		if (info) {
			uniforms[info.name] = gl.getUniformLocation(shader, info.name)
		}
	}

	return {shader, uniforms};
}

function createTexture(w, h) {
	const texture = gl.createTexture();
	gl.bindTexture(gl.TEXTURE_2D, texture);
	if (w && h) {
		gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
	}
	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
	return texture;
}

class RenderTarget {
	constructor(w, h) {
		this.width = w;
		this.height = h;
		this.framebuffer = gl.createFramebuffer();

		this.texture = createTexture(w, h);

		gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
		gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture, 0);
		
		gl.bindFramebuffer(gl.FRAMEBUFFER, null);
	}

	bind(clear=true) {
		gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
		gl.viewport(0, 0, this.width, this.height);
		if (clear) {
			gl.clearColor(1, 1, 1, 0);
			gl.clear(gl.COLOR_BUFFER_BIT);
		}
	}

	setSize(w, h) {
		if (this.width !== w || this.height !== h) {
			gl.deleteFramebuffer(this.framebuffer);
			gl.deleteTexture(this.texture);

			this.width = w;
			this.height = h;

			this.framebuffer = gl.createFramebuffer();
			this.texture = createTexture(w, h);

			gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
			gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture, 0);
			gl.bindFramebuffer(gl.FRAMEBUFFER, null);
		}
	}
}

const go = (function() {
	let lastTime = 0;
	let lastPrint = 0;
	let frames = 0;
	let fpsLastSecond = 60;
	let stopped = false;
	let totalFrames = 0;

	const fpsElem = document.getElementById('fps');
	const mspfElem = document.getElementById('mspf');

	const {shader: blitShaderProgram, uniforms: blitShaderUniforms} = 
		compileShader('alpha-mask-fs', 'pass-through-vs', { aPosition: 0 });

	const screenRenderTarget = new RenderTarget(WIDTH*DPI, HEIGHT*DPI);

	window.addEventListener('resize', () => {
		WIDTH = window.innerWidth;
		HEIGHT = window.innerHeight;
		mainCanvas.width = WIDTH*DPI;
		mainCanvas.height = HEIGHT*DPI;
		screenRenderTarget.setSize(WIDTH*DPI, HEIGHT*DPI);
	});

	const fullscreenQuadVBO = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, fullscreenQuadVBO);
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
		+1,+1, -1,+1, -1,-1, 
		-1,-1, +1,-1, +1,+1
	]), gl.STATIC_DRAW);

	function go(ts) {
		if (lastTime === 0) {
			lastTime = ts;
			lastPrint = ts;
			return requestAnimationFrame(go);
		}
		if (stopped) {
			lastTime = lastPrint = 0;
			return;
		}
		
		gl.clearColor(1.0, 1.0, 1.0, 1.0);
		gl.clear(gl.COLOR_BUFFER_BIT);
		
		requestAnimationFrame(go);

		const dt = ts-lastTime;
		lastTime = ts;
		++frames;
		++totalFrames;

		const updateStart = now();
		screenRenderTarget.bind();
		gl.enable(gl.BLEND);
		gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);

		update(dt/1000);

		gl.bindFramebuffer(gl.FRAMEBUFFER, null);
		gl.enable(gl.BLEND);
		gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);

		gl.viewport(0, 0, WIDTH*DPI, HEIGHT*DPI);

		gl.clearColor(1.0, 1.0, 1.0, 1.0);
		gl.clear(gl.COLOR_BUFFER_BIT);

		gl.useProgram(blitShaderProgram);
		gl.activeTexture(gl.TEXTURE1);
		gl.bindTexture(gl.TEXTURE_2D, screenRenderTarget.texture);
		
		gl.uniform1i(blitShaderUniforms.uTexture, 1);
		
		gl.enable(gl.BLEND);

		gl.bindBuffer(gl.ARRAY_BUFFER, fullscreenQuadVBO);
		gl.enableVertexAttribArray(0);
		gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
		gl.drawArrays(gl.TRIANGLES, 0, 6);

		


		const updateEnd = now();
		MOUSE.transitions = 0;
		if (ts - lastPrint >= 1000) {
			fpsLastSecond = frames;
			fpsElem.textContent = "f/s: "+fpsLastSecond;
			frames = 0;
			lastPrint = ts;
		}
		mspfElem.textContent = "ms/f: "+(updateEnd-updateStart).toFixed(3);
	}


	window.addEventListener('keyup', ({which}) => {
		if (which === 27) {
			stopped = !stopped
			if (!stopped) {
				requestAnimationFrame(go);
			}
		}
	});


	return go;
}())

requestAnimationFrame(go);

const update = (function() {
	// makes it easier to swap out f64array for rough benchmarking
	const BPE = Float32Array.BYTES_PER_ELEMENT;
	const buf = new ArrayBuffer(8*BPE*NUM_PARTICLES);

	const size = new Float32Array(buf, 0*BPE*NUM_PARTICLES, NUM_PARTICLES);
	const xPos = new Float32Array(buf, 1*BPE*NUM_PARTICLES, NUM_PARTICLES);
	const yPos = new Float32Array(buf, 2*BPE*NUM_PARTICLES, NUM_PARTICLES);
	const xVel = new Float32Array(buf, 3*BPE*NUM_PARTICLES, NUM_PARTICLES);
	const yVel = new Float32Array(buf, 4*BPE*NUM_PARTICLES, NUM_PARTICLES);
	const xOld = new Float32Array(buf, 5*BPE*NUM_PARTICLES, NUM_PARTICLES);	
	const yOld = new Float32Array(buf, 6*BPE*NUM_PARTICLES, NUM_PARTICLES);

    // left bound of the particle
    const sortBounds = new Float32Array(buf, 7*BPE*NUM_PARTICLES, NUM_PARTICLES);
    const sorter = new RadixSorter(NUM_PARTICLES);
    

	// don't use bpe since we always want f32array for the vertices
	const VERTEX_SIZE = 4;
	const particleVerts = new Float32Array(NUM_PARTICLES*VERTEX_SIZE*Float32Array.BYTES_PER_ELEMENT);
	const particleTris = new Uint16Array(NUM_PARTICLES*6);

	const {shader: particleShader, uniforms: {uTexture: particleTextureLoc}} = 
		compileShader('particle-fs', 'particle-vs', { 
			aPosition: 0,
			aTexcoord: 1,
		});


	const prerenderedGradients = [];
	
	const {abs, sqrt, random, floor} = Math;
	
	function nextPow2(v) {
		v--;
		v |= v >> 1;
		v |= v >> 2;
		v |= v >> 4;
		v |= v >> 8;
		v |= v >> 16;
		v++;
		return v;
	}

	const gradientTexture = (function() {
		const sz = 32;
		const prerenderedGradientCanvas = document.createElement('canvas');
		const fullsize = nextPow2(Math.floor(DPI)*sz*4);
		prerenderedGradientCanvas.width = fullsize;
		prerenderedGradientCanvas.height = fullsize;
		const prgCtx = prerenderedGradientCanvas.getContext('2d');
		const size = fullsize/4;
		
		prgCtx.save();
		{
			const rg = prgCtx.createRadialGradient(size*2, size*2, 0, size*2, size*2, size*2);
			//prgCtx.scale(DPI, DPI);
			rg.addColorStop(0, 'black');
			rg.addColorStop(1, 'transparent');

			prgCtx.fillStyle = rg;
			prgCtx.moveTo(size*2, size*2);
			prgCtx.arc(size*2, size*2, size*2, 0, Math.PI*2)
			prgCtx.fill();
		}
		prgCtx.restore();

		const texture = gl.createTexture();
		gl.bindTexture(gl.TEXTURE_2D, texture);

		gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, prerenderedGradientCanvas);

		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_LINEAR);
		
		gl.generateMipmap(gl.TEXTURE_2D);

		return texture;
	}());


	for (let i = 0; i < NUM_PARTICLES; ++i) {
		const sz = (random()*4+random()*2+random()*3+random());
		size[i] = sz;
		xPos[i] = random() * WIDTH/2;
		yPos[i] = random() * HEIGHT;
		xVel[i] = random() * 4 - 2;
		yVel[i] = random() * 8 - 4;

		const index = i*6;
		const vertex = i*4; // 4 vertices per quad. unrelated to VERTEX_SIZE
		particleTris[index+0] = vertex+0;
		particleTris[index+1] = vertex+1;
		particleTris[index+2] = vertex+2;

		particleTris[index+3] = vertex+0;
		particleTris[index+4] = vertex+2;
		particleTris[index+5] = vertex+3;

		const vi = vertex*VERTEX_SIZE;

		// uvs are constant so we can fill them out here.

		// order: {bottom left|top left|top right|bottom right}
		particleVerts[vi+VERTEX_SIZE*0+2] = 0.0;
		particleVerts[vi+VERTEX_SIZE*0+3] = 0.0;

		particleVerts[vi+VERTEX_SIZE*1+2] = 0.0;
		particleVerts[vi+VERTEX_SIZE*1+3] = 1.0;

		particleVerts[vi+VERTEX_SIZE*2+2] = 1.0;
		particleVerts[vi+VERTEX_SIZE*2+3] = 1.0;

		particleVerts[vi+VERTEX_SIZE*3+2] = 1.0;
		particleVerts[vi+VERTEX_SIZE*3+3] = 0.0;

		
		
	}

	const particleVBO = gl.createBuffer();
	const particleIBO = gl.createBuffer();

	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, particleIBO);
	gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, particleTris, gl.STATIC_DRAW);

	gl.bindBuffer(gl.ARRAY_BUFFER, particleVBO);
	gl.bufferData(gl.ARRAY_BUFFER, particleVerts, gl.DYNAMIC_DRAW);


	let target = -1;


	function findTarget(x, y) {
		let mx = x;
		let my = y;
		for (let i = 0; i < NUM_PARTICLES; ++i) {
			const d = (xPos[i]-mx)*(xPos[i]-mx) + (yPos[i]-my)*(yPos[i]-my);
			if (d <4.0 * size[i]*size[i]) {
				return i;
			}
		}
		return -1;
	}

	function checkCollisions(dt) {
	    for (let i = 0; i < NUM_PARTICLES; ++i) {
	        sortBounds[i] = xPos[i] - size[i];
	    }
	    
	    const indices = sorter.sort(sortBounds);
	    let collisions = 0;
	    
	    for (let ia = 0; ia < NUM_PARTICLES; ++ia) {
	        const a = indices[ia];
	        if (a === target) {
	            continue;
	        }

	        for (let ib = ia+1; ib < NUM_PARTICLES; ++ib) {
	            const b = indices[ib];
				let pxb = xPos[b];
				let pyb = yPos[b];
				let bSize = size[b];
				
    			let pxa = xPos[a];
    			let pya = yPos[a];
    			let aSize = size[a];
                if (pxb-bSize > pxa+aSize) {
                    break;
                }
				
				const rsum = (size[a]+size[b])*(size[a]+size[b]) + 0.2;
				const nx = pxb - pxa;
				const ny = pyb - pya;
				
				const dist2 = nx*nx + ny*ny;

				if (dist2 < rsum) {
					++collisions;
					const dist = sqrt(dist2);
					let vxa = xVel[a], vya = yVel[a];
					let vxb = xVel[b], vyb = yVel[b];

					if (abs(dist) < 0.1) {
						continue;
					}

					const id = 1.0 / dist;
					const cdx = nx * id;
					const cdy = ny * id;
					const dd = size[a] + size[b] - dist;

					const percent = 0.2;
					const slop = 0.03;
					const correctionMul = percent * Math.max(dd-slop, 0) / 2;
	
					pxa -= correctionMul*cdx;
					pya -= correctionMul*cdy;

					pxb += correctionMul*cdx;
					pyb += correctionMul*cdy;

					const aSpeed = sqrt(vxa*vxa + vya*vya);
					if (aSpeed > 0.0001) {
						const cosam = (cdx * vxa + cdy * vya) * ELASTICITY;
						vxa -= cdx*cosam;
						vya -= cdy*cosam;
						vxb += cdx*cosam;
						vyb += cdy*cosam;
					}
					// can't use vxb, vyb here since we may have changed them
					const bSpeed = sqrt(xVel[b]*xVel[b] + yVel[b]*yVel[b]);
					if (bSpeed > 0.0001) {
						const cosam = (cdx * xVel[b] + cdy * yVel[b]) * ELASTICITY;
						vxa += cdx*cosam;
						vya += cdy*cosam;
						vxb -= cdx*cosam;
						vyb -= cdy*cosam;
					}
					xPos[a] = pxa;
					xPos[b] = pxb;

					yPos[a] = pya;
					yPos[b] = pyb;

					xVel[a] = vxa;
					xVel[b] = vxb;

					yVel[a] = vya;
					yVel[b] = vyb;
				}
			}
		}
		return collisions;
	}

	function simPhysics(dt) {
		let collisions = checkCollisions(dt);

		for (let i = 0; i < NUM_PARTICLES; ++i) {
			const radius = size[i];
			if (xPos[i] <= radius) {
				xPos[i] = radius;
				xVel[i] *= -ELASTICITY;
			}
			if (xPos[i] >= WIDTH-radius) {
				xPos[i] = WIDTH-radius;
				xVel[i] *= -ELASTICITY;
			}

			if (yPos[i] <= size[i]) {
				yPos[i] = size[i];
				yVel[i] *= -ELASTICITY;
			}
			if (yPos[i] >= HEIGHT-radius) {
				yPos[i] = HEIGHT-radius;
				yVel[i] *= -ELASTICITY;
			}

			if (i !== target) {
				xVel[i] += X_ACCEL*dt;
				yVel[i] += Y_ACCEL*dt;
				xPos[i] += xVel[i]*dt;
				yPos[i] += yVel[i]*dt;
			}

			xOld[i] = xPos[i];
			yOld[i] = yPos[i];
		}

		return collisions;
	}

	function updateVertices() {

		const xScale = 1 / WIDTH;
		const yScale = 1 / HEIGHT;

		for (let i = 0; i < NUM_PARTICLES; ++i) {
			const x = xPos[i];
			const y = yPos[i];
			const r = size[i]*4;

			// order: {bottom left|top left|top right|bottom right}

			let vertexIndex = i*4*VERTEX_SIZE;

			particleVerts[vertexIndex+0] = (x-r) * xScale;
			particleVerts[vertexIndex+1] = (y-r) * yScale;

			vertexIndex += VERTEX_SIZE;

			particleVerts[vertexIndex+0] = (x-r) * xScale;
			particleVerts[vertexIndex+1] = (y+r) * yScale;

			vertexIndex += VERTEX_SIZE;

			particleVerts[vertexIndex+0] = (x+r) * xScale;
			particleVerts[vertexIndex+1] = (y+r) * yScale;

			vertexIndex += VERTEX_SIZE;
			
			particleVerts[vertexIndex+0] = (x+r) * xScale;
			particleVerts[vertexIndex+1] = (y-r) * yScale;
		}
		gl.bindBuffer(gl.ARRAY_BUFFER, particleVBO);
		gl.bufferSubData(gl.ARRAY_BUFFER, 0, particleVerts);
	}
	
	function perturb() {
		for (let i = 0; i < NUM_PARTICLES; ++i) {
			xVel[i] = (random()-0.5)*10;
			yVel[i] = (random()-0.5)*10;
		}
	}
	
	window.addEventListener('keyup', ({which}) => {
		if (which === 32) {
			perturb();
		}
	})

	function update(dt) {
		dt *= 60*TIME_SCALE;
		if (MOUSE.transitions) {
			if (MOUSE.isDown) {
				target = findTarget(MOUSE.x, MOUSE.y);
			}
			else {
				target = -1;
			}
		}
		if (target >= 0) {
			xPos[target] = MOUSE.x;
			yPos[target] = MOUSE.y;
			xVel[target] = 0.5*(xPos[target] - xOld[target]) * dt;
			yVel[target] = 0.5*(yPos[target] - yOld[target]) * dt;
		}
		
		let collisions = 0;
		for (let i = 0; i < PHYSICS_ITERATIONS; ++i) {
			collisions += simPhysics(dt/PHYSICS_ITERATIONS);
		}

		updateVertices();

		gl.useProgram(particleShader);
		gl.activeTexture(gl.TEXTURE0);
		gl.bindTexture(gl.TEXTURE_2D, gradientTexture);
		gl.uniform1i(particleTextureLoc, 0);


		gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, particleIBO);
		gl.bindBuffer(gl.ARRAY_BUFFER, particleVBO);

		gl.enableVertexAttribArray(0);
		gl.enableVertexAttribArray(1);

		gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 4*VERTEX_SIZE, 0);
		gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 4*VERTEX_SIZE, 2*4);

		gl.drawElements(gl.TRIANGLES, particleTris.length, gl.UNSIGNED_SHORT, 0);
	}

	return update;
}())



}());
            
          
!
999px
🕑 One or more of the npm packages you are using needs to be built. You're the first person to ever need it! We're building it right now and your preview will start updating again when it's ready.

Console