Monorepo for Aesthetic.Computer
aesthetic.computer
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 };