Skip to content

Instantly share code, notes, and snippets.

@graemephi
Created January 6, 2025 19:14
Show Gist options
  • Save graemephi/555180b549b88af5c5368dca322fe82e to your computer and use it in GitHub Desktop.
Save graemephi/555180b549b88af5c5368dca322fe82e to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/p5.min.js"></script>
<style>
body { margin: 0; display: flex; justify-content: center; align-items: center; height: 100vh; background: #1a1a1a; }
button { margin: 5px; padding: 8px 16px; background: #333; color: white; border: none; cursor: pointer; }
button:hover { background: #444; }
#controls { position: fixed; top: 10px; left: 50%; transform: translateX(-50%); }
</style>
</head>
<body>
<div id="controls">
<button onclick="togglePause()">Play/Pause</button>
<button onclick="clearGrid()">Clear</button>
<button onclick="randomize()">Random</button>
<button onclick="clearHeatmap()">Clear Heatmap</button>
</div>
<script>
let updateShader, renderShader, canvas;
let gridBuffers = [];
let deathBuffers = [];
let currentBuffer = 0;
let paused = false;
let DEATH_LIMIT = 256;
const gridVert = `
attribute vec3 aPosition;
attribute vec2 aTexCoord;
varying vec2 vTexCoord;
void main() {
vTexCoord = aTexCoord;
gl_Position = vec4(aPosition * 2.0 - 1.0, 1.0);
}`;
const updateFrag = `
precision highp float;
uniform sampler2D gridState;
uniform sampler2D deathState;
uniform vec2 resolution;
uniform bool updateDeath;
const float deathLimit = ${DEATH_LIMIT}.0;
float getGrid(vec2 coord) {
return texture2D(gridState, coord).r;
}
float getDeath(vec2 coord) {
return texture2D(deathState, coord).r * deathLimit;
}
void main() {
vec2 texel = 1.0 / resolution;
vec2 coord = gl_FragCoord.xy / resolution;
float currentCell = getGrid(coord);
float currentDeath = getDeath(coord);
float neighbors = 0.0;
float afterlifeNeighbors = 0.0;
float maxNeighborDeath = currentDeath;
float minNeighborDeath = currentDeath;
float avg = 0.0;
for(int dy = -1; dy <= 1; dy++) {
for(int dx = -1; dx <= 1; dx++) {
if(dx == 0 && dy == 0) continue;
vec2 offset = vec2(float(dx), float(dy)) * texel;
vec2 neighCoord = fract(coord + offset);
float neighGrid = getGrid(neighCoord);
float neighDeath = getDeath(neighCoord);
neighbors += neighGrid;
afterlifeNeighbors += float(neighDeath >= currentDeath);
maxNeighborDeath = max(maxNeighborDeath, neighDeath);
minNeighborDeath = min(minNeighborDeath, neighDeath);
avg += neighDeath;
}
}
avg /= 8.0;
float newState = 0.0;
if (neighbors == 3.0 || (currentCell > 0.5 && neighbors == 2.0)) {
newState = 1.0;
}
if (updateDeath) {
float newDeath = currentDeath;
if (afterlifeNeighbors == 3.0) {
newDeath = currentDeath * 0.5 - 1.0;
} else if (afterlifeNeighbors == 2.0 || afterlifeNeighbors == 4.0) {
newDeath = currentDeath * 0.5 + 1.0;
} else if (afterlifeNeighbors > 4.0) {
newDeath = avg;
} else {
newDeath = minNeighborDeath + (maxNeighborDeath - minNeighborDeath);
newDeath = newDeath * 0.95;
newDeath = newDeath + 1.0;
}
// Transitioning from alive to dead
if (currentCell > 0.5 && newState < 0.5) {
newDeath = avg * 0.5;
}
// Dead non-transitioning cell neighbouring an alive cell
if (currentCell < 0.5 && newState < 0.5 && neighbors >= 1.0) {
newDeath = (avg - -1.0) * 2.0 + -1.0;
}
// Preserve death count of alive non-transitioning cells
if (currentCell > 0.5 && newState > 0.5) {
newDeath = currentDeath;
}
newDeath = clamp(newDeath, 0.0, deathLimit);
gl_FragColor = vec4(newDeath / deathLimit, 0.0, 0.0, 1.0);
} else {
gl_FragColor = vec4(newState, 0.0, 0.0, 1.0);
}
}`;
const renderFrag = `
precision highp float;
uniform sampler2D gridState;
uniform sampler2D deathState1;
uniform sampler2D deathState2;
varying vec2 vTexCoord;
const vec3 startColor = vec3(20.0/255.0, 0.0, 60.0/255.0);
const vec3 endColor = vec3(120.0/255.0, 200.0/255.0, 1.0);
const float deathLimit = ${DEATH_LIMIT}.0;
void main() {
float cell = texture2D(gridState, vTexCoord).r;
float death = 0.5 * (texture2D(deathState1, vTexCoord).r + texture2D(deathState2, vTexCoord).r);
if (cell > 0.5) {
gl_FragColor = vec4(1.0);
} else if (death > 0.0) {
float intensity = sqrt(death * deathLimit + 1.0) / sqrt(deathLimit);
vec3 color = mix(startColor, endColor, intensity);
gl_FragColor = vec4(color, 1.0);
} else {
gl_FragColor = vec4(0.1, 0.1, 0.1, 1.0);
}
}`;
function setup() {
createCanvas(WIDTH, HEIGHT, WEBGL);
pixelDensity(1);
noStroke();
noSmooth();
canvas = document.querySelector("canvas");
canvas.style['image-rendering'] = 'pixelated';
canvas.style['transform'] = `scale(${CELL_SIZE})`;
updateShader = createShader(gridVert, updateFrag);
renderShader = createShader(gridVert, renderFrag);
for (let i = 0; i < 2; i++) {
gridBuffers[i] = createFramebuffer({ textureFiltering: NEAREST });
deathBuffers[i] = createFramebuffer({ textureFiltering: NEAREST });
}
randomize();
update(true);
}
function draw() {
if (!paused) update();
render();
}
function update(gridOnly = false) {
const nextBuffer = 1 - currentBuffer;
const resolution = [WIDTH, HEIGHT];
gridBuffers[nextBuffer].begin()
shader(updateShader);
updateShader.setUniform('gridState', gridBuffers[currentBuffer]);
updateShader.setUniform('deathState', deathBuffers[currentBuffer]);
updateShader.setUniform('resolution', resolution);
updateShader.setUniform('updateDeath', false);
rect(0, 0, WIDTH, HEIGHT);
gridBuffers[nextBuffer].end();
if (!gridOnly) {
deathBuffers[nextBuffer].begin();
shader(updateShader);
updateShader.setUniform('gridState', gridBuffers[currentBuffer]);
updateShader.setUniform('deathState', deathBuffers[currentBuffer]);
updateShader.setUniform('resolution', resolution);
updateShader.setUniform('updateDeath', true);
rect(0, 0, WIDTH, HEIGHT);
deathBuffers[nextBuffer].end();
}
currentBuffer = nextBuffer;
}
function render() {
shader(renderShader);
renderShader.setUniform('gridState', gridBuffers[currentBuffer]);
renderShader.setUniform('deathState1', deathBuffers[currentBuffer]);
renderShader.setUniform('deathState2', deathBuffers[1 - currentBuffer]);
rect(0, 0, WIDTH, HEIGHT);
}
function mousePressed() {
}
function mouseDragged() {
mousePressed();
}
function togglePause() {
paused = !paused;
}
function setColor(buf, color255) {
buf.loadPixels();
for (let i = 0; i < buf.pixels.length; i += 4) {
buf.pixels[i] = color255;
buf.pixels[i + 1] = 0;
buf.pixels[i + 2] = 0;
buf.pixels[i + 3] = 255;
}
buf.updatePixels();
}
function clearGrid() {
setColor(gridBuffers[currentBuffer], 0);
}
function clearHeatmap() {
setColor(deathBuffers[currentBuffer], 0);
}
function randomize() {
clearGrid();
clearHeatmap();
let buf = gridBuffers[currentBuffer];
buf.loadPixels();
for (let y = 0; y < HEIGHT / 2; ++y) {
for (let x = 0; x < WIDTH / 2; ++x) {
let i = (y * WIDTH + x) * 4 + ((WIDTH / 4) * HEIGHT) * 4 + WIDTH;
buf.pixels[i] = Math.random() < 0.075 ? 255 : 0;
buf.pixels[i + 1] = 0;
buf.pixels[i + 2] = 0;
buf.pixels[i + 3] = 255;
}
}
buf.updatePixels();
}
const CELL_SIZE = 2;
const WIDTH = 256;
const HEIGHT = 256;
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment