Created
January 6, 2025 19:14
-
-
Save graemephi/555180b549b88af5c5368dca322fe82e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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