Monorepo for Aesthetic.Computer aesthetic.computer
at main 278 lines 8.0 kB view raw
1// gameoflife.mjs - Conway's Game of Life with pixel manipulation 2// For recording gifs and mp4s with the orchestrator 3 4const { floor, random } = Math; 5 6let time = 0; 7let cellSize = 4; // Each cell is 4x4 pixels 8let grid = null; 9let nextGrid = null; 10let width = 0; 11let height = 0; 12let generation = 0; 13 14// Color schemes that cycle over time 15const colorSchemes = [ 16 { alive: [0, 255, 0], dead: [0, 20, 0] }, // Green matrix 17 { alive: [255, 255, 255], dead: [0, 0, 0] }, // Classic B&W 18 { alive: [255, 100, 0], dead: [20, 5, 0] }, // Fire 19 { alive: [0, 150, 255], dead: [0, 10, 30] }, // Blue 20 { alive: [255, 0, 255], dead: [30, 0, 30] }, // Magenta 21]; 22 23function paint({ api, frameIndex = 0, frameTime = 0, simCount = 0n }) { 24 const { screen } = api; 25 time += 0.02; 26 27 // Initialize grid on first frame 28 if (!grid) { 29 initializeGrid(screen); 30 } 31 32 // Update game of life every few frames for visibility 33 if (frameIndex % 3 === 0) { 34 updateGameOfLife(); 35 generation++; 36 37 // Reset with new random pattern every 200 generations 38 if (generation % 200 === 0) { 39 seedRandomPattern(); 40 } 41 } 42 43 // Choose color scheme based on time 44 const schemeIndex = floor(time * 0.1) % colorSchemes.length; 45 const scheme = colorSchemes[schemeIndex]; 46 47 // Render the grid to pixels 48 renderGridToPixels(screen, scheme); 49 50 // Add some sparkle effects 51 addSparkleEffects(screen, time); 52} 53 54function initializeGrid(screen) { 55 width = floor(screen.width / cellSize); 56 height = floor(screen.height / cellSize); 57 58 grid = new Array(height); 59 nextGrid = new Array(height); 60 61 for (let y = 0; y < height; y++) { 62 grid[y] = new Array(width); 63 nextGrid[y] = new Array(width); 64 for (let x = 0; x < width; x++) { 65 grid[y][x] = false; 66 nextGrid[y][x] = false; 67 } 68 } 69 70 // Seed with some interesting patterns 71 seedInitialPatterns(); 72} 73 74function seedInitialPatterns() { 75 // Clear grid 76 for (let y = 0; y < height; y++) { 77 for (let x = 0; x < width; x++) { 78 grid[y][x] = false; 79 } 80 } 81 82 // Add glider guns 83 addGliderGun(10, 10); 84 if (width > 100) addGliderGun(width - 50, 10); 85 if (height > 100) addGliderGun(10, height - 50); 86 87 // Add some random noise 88 for (let i = 0; i < width * height * 0.1; i++) { 89 const x = floor(random() * width); 90 const y = floor(random() * height); 91 grid[y][x] = true; 92 } 93} 94 95function seedRandomPattern() { 96 // Create interesting random patterns 97 const patternType = floor(random() * 3); 98 99 // Clear grid first 100 for (let y = 0; y < height; y++) { 101 for (let x = 0; x < width; x++) { 102 grid[y][x] = false; 103 } 104 } 105 106 if (patternType === 0) { 107 // Random soup 108 for (let y = 0; y < height; y++) { 109 for (let x = 0; x < width; x++) { 110 grid[y][x] = random() < 0.4; 111 } 112 } 113 } else if (patternType === 1) { 114 // Centered explosion 115 const centerX = floor(width / 2); 116 const centerY = floor(height / 2); 117 const radius = floor(Math.min(width, height) / 4); 118 119 for (let y = centerY - radius; y < centerY + radius; y++) { 120 for (let x = centerX - radius; x < centerX + radius; x++) { 121 if (x >= 0 && x < width && y >= 0 && y < height) { 122 const dist = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2); 123 grid[y][x] = random() < 0.6 && dist < radius; 124 } 125 } 126 } 127 } else { 128 // Vertical stripes with noise 129 for (let x = 0; x < width; x += 8) { 130 for (let y = 0; y < height; y++) { 131 if (x < width) { 132 grid[y][x] = random() < 0.7; 133 if (x + 1 < width) grid[y][x + 1] = random() < 0.7; 134 } 135 } 136 } 137 } 138} 139 140function addGliderGun(startX, startY) { 141 // Gosper glider gun pattern 142 const pattern = [ 143 [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0], 144 [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0], 145 [0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1], 146 [0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1], 147 [1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 148 [1,1,0,0,0,0,0,0,0,0,1,0,0,0,1,0,1,1,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0], 149 [0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0], 150 [0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 151 [0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 152 ]; 153 154 for (let y = 0; y < pattern.length; y++) { 155 for (let x = 0; x < pattern[y].length; x++) { 156 const gridX = startX + x; 157 const gridY = startY + y; 158 if (gridX < width && gridY < height && gridX >= 0 && gridY >= 0) { 159 grid[gridY][gridX] = pattern[y][x] === 1; 160 } 161 } 162 } 163} 164 165function updateGameOfLife() { 166 // Calculate next generation 167 for (let y = 0; y < height; y++) { 168 for (let x = 0; x < width; x++) { 169 const neighbors = countNeighbors(x, y); 170 const isAlive = grid[y][x]; 171 172 if (isAlive) { 173 // Live cell survives with 2 or 3 neighbors 174 nextGrid[y][x] = neighbors === 2 || neighbors === 3; 175 } else { 176 // Dead cell becomes alive with exactly 3 neighbors 177 nextGrid[y][x] = neighbors === 3; 178 } 179 } 180 } 181 182 // Swap grids 183 const temp = grid; 184 grid = nextGrid; 185 nextGrid = temp; 186} 187 188function countNeighbors(x, y) { 189 let count = 0; 190 for (let dy = -1; dy <= 1; dy++) { 191 for (let dx = -1; dx <= 1; dx++) { 192 if (dx === 0 && dy === 0) continue; // Skip self 193 194 const nx = x + dx; 195 const ny = y + dy; 196 197 // Wrap around edges for toroidal topology 198 const wrappedX = (nx + width) % width; 199 const wrappedY = (ny + height) % height; 200 201 if (grid[wrappedY][wrappedX]) { 202 count++; 203 } 204 } 205 } 206 return count; 207} 208 209function renderGridToPixels(screen, colorScheme) { 210 for (let y = 0; y < height; y++) { 211 for (let x = 0; x < width; x++) { 212 const isAlive = grid[y][x]; 213 const color = isAlive ? colorScheme.alive : colorScheme.dead; 214 215 // Fill the cell area 216 for (let py = 0; py < cellSize; py++) { 217 for (let px = 0; px < cellSize; px++) { 218 const screenX = x * cellSize + px; 219 const screenY = y * cellSize + py; 220 221 if (screenX < screen.width && screenY < screen.height) { 222 const pixelIndex = (screenY * screen.width + screenX) * 4; 223 screen.pixels[pixelIndex] = color[0]; // R 224 screen.pixels[pixelIndex + 1] = color[1]; // G 225 screen.pixels[pixelIndex + 2] = color[2]; // B 226 screen.pixels[pixelIndex + 3] = 255; // A 227 } 228 } 229 } 230 } 231 } 232} 233 234function addSparkleEffects(screen, time) { 235 // Add occasional sparkles on living cells 236 const sparkleCount = 20; 237 238 for (let i = 0; i < sparkleCount; i++) { 239 const x = floor(random() * width); 240 const y = floor(random() * height); 241 242 if (grid[y][x]) { // Only sparkle on living cells 243 const sparkleX = x * cellSize + floor(random() * cellSize); 244 const sparkleY = y * cellSize + floor(random() * cellSize); 245 246 if (sparkleX < screen.width && sparkleY < screen.height) { 247 const pixelIndex = (sparkleY * screen.width + sparkleX) * 4; 248 const intensity = 128 + floor(127 * Math.sin(time * 5 + i)); 249 250 screen.pixels[pixelIndex] = 255; // R 251 screen.pixels[pixelIndex + 1] = 255; // G 252 screen.pixels[pixelIndex + 2] = 255; // B 253 screen.pixels[pixelIndex + 3] = intensity; // A 254 } 255 } 256 } 257} 258 259function act({ event, api }) { 260 // Reset with new pattern on spacebar 261 if (event.key === " " || event.key === "Spacebar") { 262 seedRandomPattern(); 263 generation = 0; 264 } 265 266 // Adjust cell size 267 if (event.key === "ArrowUp") { 268 cellSize = Math.min(8, cellSize + 1); 269 initializeGrid(api.screen); 270 } 271 if (event.key === "ArrowDown") { 272 cellSize = Math.max(2, cellSize - 1); 273 initializeGrid(api.screen); 274 } 275} 276 277// Export the piece 278export { paint, act };