- width height layers of single bit pixels
- 32 bit data words contained in single Uint32Array
- drawShape stepConwayLife cellular automata
- getPixel(x,y,layer)
- setPixel(x,y,layer,state)
- cool(falloff)
- heat(layer,value)
- drawMask(strings,maskChar,x,y,layer)
- rect(x,y,width,height,layer=0)
- drawGrid(skipx=20,skipy=10,layer=0)
- writePixels(pixels,x,y,layer)
- stepConwayLife(readLayer,writeLayer)
- copyLayer(readLayer,writeLayer)
- countNeighbors(x,y,layer)
Cellular Automata and friends are welcome to visit conway life on bitgrid
// bitgrid.js
// (c) 2026 nitrlogic
// All rights reserved
// - width height layers of single bit pixels
// - 32 bit data words contained in single Uint32Array
// - drawShape stepConwayLife cellular automata functions incoming
class BitGrid {
constructor(width,height,layers) {
this.width = width;
this.height = height;
this.layers = layers;
this.span=(width+31)>>5;
this.data=new Uint32Array(this.span*height*layers);
this.heatmap=new Uint16Array(width*height);
}
// bit pixel
getPixel(x,y,layer){
// x,y toroidal wrap around getter
x = (x + this.width) % this.width;
y = (y + this.height) % this.height;
const offset = layer*this.height*this.span;
const wordIndex = y*this.span+(x>>5);
const bitIndex = x&31;
const word=this.data[offset+wordIndex];
return (word&(1<<bitIndex))!=0;
}
setPixel(x,y,layer,state){
const offset=layer*this.height*this.span+y*this.span+(x>>5);
const mask=1<<(x&31);
let word=this.data[offset];
if(state){
word|=mask;
}else{
word&=~mask;
}
this.data[offset]=word
}
// heatmap methods
cool(falloff){
let index=0;
for(let y=0;y<this.height;y++){
for(let x=0;x<this.width;x++){
this.heatmap[index++]*=falloff;
}
}
}
heat(layer,value){
let offset=layer*this.height*this.span;
let index=0;
let word=0;
for(let y=0;y<this.height;y++){
for(let x=0;x<this.width;x++){
if((x&31)==0){
word=this.data[offset++];
}
const mask=1<<(x&31);
if(word&mask) this.heatmap[index]+=value;
index++;
}
}
}
drawMask(strings,maskChar,x,y,layer){
for(const text of strings){
for(let i=0;i<text.length;i++){
const char=text[i];
const state=(char==maskChar);//(char=="O");
this.setPixel(x+i,y,layer,state);
}
y++;
}
}
rect(x,y,width,height,layer=0){
const offset=layer*this.height*this.span;
for (let row = y; row < y + height; row++) {
for (let col = x; col < x + width; col++) {
const wordIndex = row * this.span + (col >> 5);
const bitIndex = col & 31;
this.data[offset+wordIndex] |= (1 << bitIndex);
}
}
}
drawGrid(skipx=20,skipy=10,layer=0){
let w=this.width;
let h=this.height;
for(let x=0;x<w;x+=skipx){
this.rect(x,2,1,h-2,layer);
}
this.rect(w-3,3,2,h-4,layer);
for(let y=0;y<h;y+=skipy){
this.rect(0,y,w,1,layer);
}
this.rect(3,h-3,w-4,2,layer);
}
writePixels(pixels,x,y,layer){
let offset=layer*this.height*this.span+y*this.span+(x>>5);
let word=this.data[offset];
for(let i=0;i<pixels.length;i++){
const mask=1<<(x&31);
const state=pixels[i];
if(state){
word|=mask;
}else{
word&=~mask;
}
x++;
if(i<pixels.length-1 && ((x&31)==0)){
this.data[offset++]=word;
word=this.data[offset];
}
}
this.data[offset]=word
}
stepConwayLife(readLayer, writeLayer) {
let entropy=0;
const w = this.width;
const h = this.height;
const pixels = new Array(w);//.fill(false);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const alive = this.getPixel(x, y, readLayer);
const neighbors = this.countNeighbors(x, y, readLayer);
const next = (alive && (neighbors === 2 || neighbors === 3)) || (!alive && neighbors === 3);
if(next!=alive) entropy++;
pixels[x]=next;
}
this.writePixels(pixels, 0, y, writeLayer);
}
return entropy;
}
copyLayer(readLayer, writeLayer) {
const wordsPerLayer = this.height * this.span;
const readOffset = readLayer * wordsPerLayer;
const writeOffset = writeLayer * wordsPerLayer;
this.data.copyWithin(writeOffset, readOffset, readOffset + wordsPerLayer);
}
countNeighbors(x, y, layer) {
let count=0;
if (this.getPixel(x-1, y-1, layer)) count++;
if (this.getPixel(x, y-1, layer)) count++;
if (this.getPixel(x+1, y-1, layer)) count++;
if (this.getPixel(x-1, y, layer)) count++;
if (this.getPixel(x+1, y, layer)) count++;
if (this.getPixel(x-1, y+1, layer)) count++;
if (this.getPixel(x, y+1, layer)) count++;
if (this.getPixel(x+1, y+1, layer)) count++;
return count;
}
};
// gridlife.js
// (c) 2026 nitrlogic
// All rights reserved
// requestAnimationFrame
"use strict"
const gridTitle="gridlife 0.2.3";
const help="cursor to pan , 0-9 speed";
function echo(...args){
const lines=[];
for(const arg of args){
const line=arg?.toString();
lines.push(line);
}
const text=lines.join(" ");
console.log("[GRIDLIFE]",text);
}
let dotBlocks=["⚫","🟠","🟡","🟢","🔴","🔵","🟣","🟤","🟧","🟨","🟩","🟥","🟦","🟪","🟫","🧡","💛","💚","💙","💜","🤎"];
const dotBlockWide=2;
const friendEmoji="🐨🐼🐸🐰🐭🐯🐱🐶🐵🐥🐷🦧🐺🦊🦝🦁🦉";
const friends=[...friendEmoji];
let displayDirty=true;
let gridTick=0;
function setSpeed(digit){
let speed=(digit);
gridTick=5*(speed*speed);
}
let gridWidth=22*8*4;
let gridHeight=23*8;
let vidWidth=72*2;
let vidHeight=22;
// todo - clear display daggs
function resizeTerminal(){
const w=terminal.clientWidth;
const h=terminal.clientHeight;
let vw=((w/8)|0)-12; //10
let vh=((h/15)|0)-2;
if((vidWidth!=vw)||(vidHeight!=vh)){
vidWidth=vw;
vidHeight=vh;
displayDirty=true;
}
}
const UPDOWN=0;
const LEFTRIGHT=1;
const pump=[0,0];
let terminal;
let startTime = performance.now();
let previousMillis = startTime;
let totalElapsed = 0;
let gridTime = 0;
function updateGrid(requestAnimationFrame_timestamp) { //was tick
resizeTerminal();
const millis=performance.now();
const elapsed = millis - previousMillis;
previousMillis=millis;
totalElapsed+=elapsed;
if(gridTime<totalElapsed){
stepFrame();
gridTime+=gridTick;
}
terminal.value=gridTitle+" "+help+"\n"+frameBlocks();
const keys=
(pressedKeys["ArrowUp"]?1:0)|
(pressedKeys["ArrowDown"]?2:0)|
(pressedKeys["ArrowRight"]?8:0)|
(pressedKeys["ArrowLeft"]?4:0);
updatePumps(keys,millis);
updateCursor();
requestAnimationFrame(updateGrid);
}
const pressedKeys={};
function onKeyUp(e){
const key = e.key;
const code = e.code;
pressedKeys[key]=false;
}
function onKeyDown(e){
const key = e.key;
const code = e.code;
pressedKeys[key]=true;
if (key === "Enter") {
e.preventDefault(); // Stop newline creation
echo("Execute terminal command!");
}
else if (key === "Tab") {
e.preventDefault(); // Stop focus from leaving the textarea
echo("Trigger auto-complete!");
}
else if ((key>="0")&&(key<="9")) {
// echo("Number speed",key|0);
setSpeed(key|0);
}
else{
echo("Key Down",{key,code});
}
}
let mouseLock=false;
let mouseSensitivity=1.0;
function inGutter(e){
const gutter=50;
const x=e.clientX;
const y=e.clientY;
const mx=0;
const my=0;
if(x<gutter) mx-=mouseSensitivity;
if(y<gutter) my-=mouseSensitivity;
return false;
}
function onMouseDown(e){
e.preventDefault();
mouseLock=!mouseLock;
// echo({e});
}
function onMouseUp(e){
// echo({e});
}
function onMouseMove(e){
e.preventDefault();
// return inGutter(e);
if(mouseLock){
const x=e.movementX*mouseSensitivity;
const y=e.movementY*mouseSensitivity;
pump[1]+=x;
pump[0]+=y;
// echo({x,y});
}
}
let previous=[];
let touchSensitivity=5.0;
function onTouchStart(e){
e.preventDefault();
previous=[];
}
function onTouchEnd(e){
previous=[];
}
function onTouchMove(e){
e.preventDefault();
const t = e.touches[0];
const tx=t.clientX;
const ty=t.clientY;
if(previous.length){
const dx=tx-previous[0];
const dy=ty-previous[1];
pump[0]+=touchSensitivity*dy;
pump[1]+=touchSensitivity*dx;
}
previous=[tx,ty];
}
function mirror(shape){
let result=[];
for(let line of shape){
result.push(line.split("").reverse().join(""));
}
return result;
}
// four directions of shape using x y symmetry flips
function axis(glider){
return [
glider,
glider.toReversed(),
mirror(glider),
mirror(glider).toReversed()
];
}
const bitgrid = new BitGrid(gridWidth,gridHeight,4);
//bitgrid.rect(4,2,2,20);
//bitgrid.rect(gridWidth/4-10,4,8,20);
let blinker=conway.shapes.oscillators.blinker;
let beacon=conway.shapes.oscillators.beacon;
let pent=conway.shapes.methuselahs.rPentomino;
let pulsar=conway.shapes.oscillators.pulsar;
const glider=axis(conway.shapes.spaceships.glider);
function draw(shape,x,y,layer){
bitgrid.drawMask(shape,"O",x,y,layer);
}
/*
let keys=Object.keys(conway.shapes.still);
let x=10;
for(let index of keys){
const still=conway.shapes.still[index];
draw(still,x,80,2);
x+=12;
}
*/
let keys1=Object.keys(conway.shapes.oscillators);
let x1=10;
for(let index of keys1){
const shape=conway.shapes.oscillators[index];
draw(shape,x1,100,2);
x1+=12;
}
//draw(beacon,10,10,2);
//draw(pent,100,14,2);
draw(glider[0],20,35,2);
draw(glider[1],20,30,2);
draw(glider[2],10,30,2);
draw(glider[3],10,20,2);
for(let i=0;i<12;i++){
for(let j=0;j<5;j++){
draw(pulsar,62+i*25,14+j*17,2);
}
}
bitgrid.stepConwayLife(2,3);
let cursorX=0;
let cursorVX=0;
let cursorY=0;
let cursorVY=0;
function updateCursor(){
cursorVX+=(pump[LEFTRIGHT])/400;
cursorVY+=(pump[UPDOWN])/400;
cursorX+=cursorVX;
if(cursorX<0){
cursorX=0;cursorVX=0;
}
let w=bitgrid.width-vidWidth;
if(w<10) w=10;
if(cursorX>=w){
cursorX=w;
cursorVX=0;
}
let h=bitgrid.height*4-vidHeight*4;
cursorY+=cursorVY;
if(cursorY<0){
cursorY=0;
cursorVY=0;
}
if(cursorY>h){
cursorY=h;
cursorVY=0;
}
cursorVX *= 0.9;
cursorVY *= 0.9;
}
function resetGrid(){
}
function gridHeatmap12(){
const result=[];
for(let i=0;i<4096;i++){
const r=((i>>8)&15)*12;
const g=((i>>4)&15)*12;
const b=((i>>0)&15)*12;
const line=""+r+";"+g+";"+b;
result[i]=line;
}
return result;
}
function gridHeatmap(){
const result=[];
for(let i=0;i<512;i++){
const r=((i>>6)&7)*18;
const g=((i>>3)&7)*18;
const b=((i>>0)&7)*18;
const line=""+r+";"+g+";"+b;
result[i]=line;
}
return result;
}
const heatRGBColors=gridHeatmap();
function heatRGB(heat){
const n=heatRGBColors.length-1;
let h=heat|0;
if(h<0) h=0;
if(h>n) h=n;
return heatRGBColors[h];
}
function gridDotWindowLayer(grid,dots,wx,wy,ww,wh){
const n=dots.length;
const w=grid.width;
const heat=grid.heatmap;
const result=[];
for(let y=0;y<wh;y++){
let offset=(wy+y)*w+wx;
let line=""
for(let x=0;x<ww;x++){
const h=(heat[offset])|0;
const index=h?(1+(h%(n-1))):0;
line+=dots[index];
offset++;
}
result.push(line);
}
return result;
}
function fadePumps(){
const previous = [...pump];
for(let index=0;index<pump.length;index++){
let integral=pump[index]|0;
let fade=(integral>>3);
integral=(fade)?integral-fade:0;
pump[index]=integral;
}
return previous;
}
function updatePumps(keys,millis){
const m=(millis|0)*0.00004;
if(keys&1) pump[UPDOWN]-=100*m;
if(keys&2) pump[UPDOWN]+=100*m;
if(keys&4) pump[LEFTRIGHT]-=72*m;
if(keys&8) pump[LEFTRIGHT]+=72*m;
fadePumps();
}
let mainMenu=false;//true;
function menuWall(blocks){
let result=[];
for(let row of blocks){
result.push("*****"+row);
}
return result.join("\n")
}
function backSpace(){
// mainMenu=!mainMenu;
draw(glider[0],20,35,2);
}
function flattenChunks(chunks) {
const count = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
const result = new Uint8Array(count);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
}
let layer=0;
let count=0;
let entropy=0;
function stepFrame(){
count++;
if(true){//((count++)&7)==5){
layer=1-layer;
entropy=bitgrid.stepConwayLife(2+layer,3-layer);
bitgrid.heat(3-layer,25);
}
bitgrid.cool(0.95);
}
function frameBlocks(){
let panx=cursorX>>1;
let pany=cursorY>>2;
let blocks=gridDotWindowLayer(bitgrid,dotBlocks,panx,pany,vidWidth/dotBlockWide,vidHeight);
return blocks.join("\n");
}
function testFrame(){
count++;
if(true){//((count++)&7)==5){
layer=1-layer;
entropy=bitgrid.stepConwayLife(2+layer,3-layer);
bitgrid.heat(3-layer,25);
}
bitgrid.cool(0.95);
let panx=cursorX>>1;
let pany=cursorY>>2;
let blocks=gridDotWindowLayer(bitgrid,dotBlocks,panx,pany,vidWidth/dotBlockWide,vidHeight);
return blocks.join("\n");
}
function initGridLife(){
terminal=document.getElementById("gridlife");
// terminal.value+="\n123\n"+friends[1]+"\n";
echo("[GRIDLIFE] initGridLife");
setSpeed(5);
resizeTerminal();
requestAnimationFrame(updateGrid);
terminal.addEventListener("mousedown",onMouseDown);
terminal.addEventListener("mouseup",onMouseUp);
terminal.addEventListener("mousemove",onMouseMove);
terminal.addEventListener("keydown",onKeyDown);
terminal.addEventListener("keyup",onKeyUp);
terminal.addEventListener("touchmove",onTouchMove);
terminal.addEventListener("touchstart",onTouchStart);
terminal.addEventListener("touchend",onTouchEnd);
}
