1
2
3
4
5
6
7
8
9
10
11
12
13(function (global) {
14 'use strict';
15
16
17
18
19
20
21 function makeGrid(cols, rows) {
22 const grid = new Array(rows);
23 for (let r = 0; r < rows; r++) {
24 grid[r] = new Array(cols).fill(0);
25 }
26 return grid;
27 }
28
29
30 function countNeighbors(grid, row, col, wrap) {
31 const rows = grid.length;
32 const cols = grid[0].length;
33 let count = 0;
34 for (let dr = -1; dr <= 1; dr++) {
35 for (let dc = -1; dc <= 1; dc++) {
36 if (dr === 0 && dc === 0) continue;
37 let r = row + dr;
38 let c = col + dc;
39 if (wrap) {
40 r = (r + rows) % rows;
41 c = (c + cols) % cols;
42 } else if (r < 0 || r >= rows || c < 0 || c >= cols) {
43 continue;
44 }
45 count += grid[r][c];
46 }
47 }
48 return count;
49 }
50
51
52 function step(grid, wrap) {
53 const rows = grid.length;
54 const cols = grid[0].length;
55 const next = makeGrid(cols, rows);
56 for (let r = 0; r < rows; r++) {
57 for (let c = 0; c < cols; c++) {
58 const neighbours = countNeighbors(grid, r, c, wrap);
59 const alive = grid[r][c] === 1;
60 if (alive && (neighbours === 2 || neighbours === 3)) {
61 next[r][c] = 1;
62 } else if (!alive && neighbours === 3) {
63 next[r][c] = 1;
64 }
65 }
66 }
67 return next;
68 }
69
70
71 function population(grid) {
72 let total = 0;
73 for (let r = 0; r < grid.length; r++) {
74 for (let c = 0; c < grid[r].length; c++) {
75 total += grid[r][c];
76 }
77 }
78 return total;
79 }
80
81
82
83
84
85
86 function placePattern(grid, cells, atRow, atCol, wrap) {
87 const rows = grid.length;
88 const cols = grid[0].length;
89 for (let i = 0; i < cells.length; i++) {
90 let r = atRow + cells[i][0];
91 let c = atCol + cells[i][1];
92 if (wrap) {
93 r = (r + rows) % rows;
94 c = (c + cols) % cols;
95 } else if (r < 0 || r >= rows || c < 0 || c >= cols) {
96 continue;
97 }
98 grid[r][c] = 1;
99 }
100 return grid;
101 }
102
103
104 const PATTERNS = {
105 glider: {
106 name: 'Glider',
107 cells: [[0, 1], [1, 2], [2, 0], [2, 1], [2, 2]],
108 },
109 pulsar: {
110 name: 'Pulsar',
111 cells: [
112 [0, 2], [0, 3], [0, 4], [0, 8], [0, 9], [0, 10],
113 [2, 0], [2, 5], [2, 7], [2, 12],
114 [3, 0], [3, 5], [3, 7], [3, 12],
115 [4, 0], [4, 5], [4, 7], [4, 12],
116 [5, 2], [5, 3], [5, 4], [5, 8], [5, 9], [5, 10],
117 [7, 2], [7, 3], [7, 4], [7, 8], [7, 9], [7, 10],
118 [8, 0], [8, 5], [8, 7], [8, 12],
119 [9, 0], [9, 5], [9, 7], [9, 12],
120 [10, 0], [10, 5], [10, 7], [10, 12],
121 [12, 2], [12, 3], [12, 4], [12, 8], [12, 9], [12, 10],
122 ],
123 },
124 gliderGun: {
125 name: 'Gosper Glider Gun',
126 cells: [
127 [0, 24],
128 [1, 22], [1, 24],
129 [2, 12], [2, 13], [2, 20], [2, 21], [2, 34], [2, 35],
130 [3, 11], [3, 15], [3, 20], [3, 21], [3, 34], [3, 35],
131 [4, 0], [4, 1], [4, 10], [4, 16], [4, 20], [4, 21],
132 [5, 0], [5, 1], [5, 10], [5, 14], [5, 16], [5, 17], [5, 22], [5, 24],
133 [6, 10], [6, 16], [6, 24],
134 [7, 11], [7, 15],
135 [8, 12], [8, 13],
136 ],
137 },
138 };
139
140 const Life = {
141 makeGrid: makeGrid,
142 countNeighbors: countNeighbors,
143 step: step,
144 population: population,
145 placePattern: placePattern,
146 PATTERNS: PATTERNS,
147 };
148
149 if (typeof module !== 'undefined' && module.exports) {
150 module.exports = Life;
151 }
152 if (typeof global !== 'undefined') {
153 global.Life = Life;
154 }
155
156
157
158
159
160 if (typeof document === 'undefined') return;
161
162 function initUI() {
163 const COLS = 90;
164 const ROWS = 55;
165 const CELL = 12;
166
167 const canvas = document.getElementById('grid');
168 const ctx = canvas.getContext('2d');
169 canvas.width = COLS * CELL;
170 canvas.height = ROWS * CELL;
171
172 const playPauseBtn = document.getElementById('playPause');
173 const stepBtn = document.getElementById('step');
174 const clearBtn = document.getElementById('clear');
175 const randomBtn = document.getElementById('random');
176 const speedInput = document.getElementById('speed');
177 const speedValue = document.getElementById('speedValue');
178 const wrapInput = document.getElementById('wrap');
179 const generationEl = document.getElementById('generation');
180 const populationEl = document.getElementById('population');
181 const patternButtons = document.getElementById('patternButtons');
182 const drawBtn = document.getElementById('drawMode');
183
184
185 const css = getComputedStyle(document.documentElement);
186 const COLOR_BG = css.getPropertyValue('--grid-bg').trim() || '#0d1117';
187 const COLOR_LINE = css.getPropertyValue('--grid-line').trim() || '#1b2430';
188 const COLOR_CELL = css.getPropertyValue('--cell').trim() || '#3fb950';
189 const COLOR_PREVIEW = css.getPropertyValue('--cell-preview').trim() || 'rgba(88,166,255,0.45)';
190
191 let grid = Life.makeGrid(COLS, ROWS);
192 let generation = 0;
193 let running = false;
194 let speed = parseInt(speedInput.value, 10);
195 let selectedPattern = null;
196
197
198 let painting = false;
199 let paintValue = 1;
200
201
202 let hoverCell = null;
203
204
205 function render() {
206 ctx.fillStyle = COLOR_BG;
207 ctx.fillRect(0, 0, canvas.width, canvas.height);
208
209
210 ctx.strokeStyle = COLOR_LINE;
211 ctx.lineWidth = 1;
212 ctx.beginPath();
213 for (let c = 0; c <= COLS; c++) {
214 const x = c * CELL + 0.5;
215 ctx.moveTo(x, 0);
216 ctx.lineTo(x, canvas.height);
217 }
218 for (let r = 0; r <= ROWS; r++) {
219 const y = r * CELL + 0.5;
220 ctx.moveTo(0, y);
221 ctx.lineTo(canvas.width, y);
222 }
223 ctx.stroke();
224
225
226 ctx.fillStyle = COLOR_CELL;
227 for (let r = 0; r < ROWS; r++) {
228 for (let c = 0; c < COLS; c++) {
229 if (grid[r][c]) {
230 ctx.fillRect(c * CELL + 1, r * CELL + 1, CELL - 1, CELL - 1);
231 }
232 }
233 }
234
235
236 if (selectedPattern && hoverCell) {
237 const cells = Life.PATTERNS[selectedPattern].cells;
238 ctx.fillStyle = COLOR_PREVIEW;
239 for (let i = 0; i < cells.length; i++) {
240 const r = hoverCell.row + cells[i][0];
241 const c = hoverCell.col + cells[i][1];
242 if (r >= 0 && r < ROWS && c >= 0 && c < COLS) {
243 ctx.fillRect(c * CELL + 1, r * CELL + 1, CELL - 1, CELL - 1);
244 }
245 }
246 }
247 }
248
249 function updateStats() {
250 generationEl.textContent = String(generation);
251 populationEl.textContent = String(Life.population(grid));
252 }
253
254 function draw() {
255 render();
256 updateStats();
257 }
258
259
260 let lastStepTime = 0;
261 let rafId = null;
262
263 function advance() {
264 grid = Life.step(grid, wrapInput.checked);
265 generation++;
266 }
267
268 function loop(timestamp) {
269 if (!running) return;
270 const interval = 1000 / speed;
271 if (timestamp - lastStepTime >= interval) {
272 lastStepTime = timestamp;
273 advance();
274 draw();
275 }
276 rafId = requestAnimationFrame(loop);
277 }
278
279 function setRunning(next) {
280 running = next;
281 playPauseBtn.textContent = running ? 'Pause' : 'Play';
282 playPauseBtn.classList.toggle('active', running);
283 if (running) {
284 lastStepTime = 0;
285 rafId = requestAnimationFrame(loop);
286 } else if (rafId !== null) {
287 cancelAnimationFrame(rafId);
288 rafId = null;
289 }
290 }
291
292
293 function cellFromEvent(evt) {
294 const rect = canvas.getBoundingClientRect();
295 const scaleX = canvas.width / rect.width;
296 const scaleY = canvas.height / rect.height;
297 const x = (evt.clientX - rect.left) * scaleX;
298 const y = (evt.clientY - rect.top) * scaleY;
299 const col = Math.floor(x / CELL);
300 const row = Math.floor(y / CELL);
301 if (row < 0 || row >= ROWS || col < 0 || col >= COLS) return null;
302 return { row: row, col: col };
303 }
304
305
306 canvas.addEventListener('mousedown', function (evt) {
307 const cell = cellFromEvent(evt);
308 if (!cell) return;
309 if (selectedPattern) {
310
311 const cells = Life.PATTERNS[selectedPattern].cells;
312 Life.placePattern(grid, cells, cell.row, cell.col, wrapInput.checked);
313 draw();
314 return;
315 }
316
317 painting = true;
318 paintValue = grid[cell.row][cell.col] ? 0 : 1;
319 grid[cell.row][cell.col] = paintValue;
320 draw();
321 });
322
323 canvas.addEventListener('mousemove', function (evt) {
324 const cell = cellFromEvent(evt);
325 if (selectedPattern) {
326 hoverCell = cell;
327 draw();
328 return;
329 }
330 if (!painting || !cell) return;
331 grid[cell.row][cell.col] = paintValue;
332 draw();
333 });
334
335 canvas.addEventListener('mouseleave', function () {
336 hoverCell = null;
337 if (selectedPattern) draw();
338 });
339
340 window.addEventListener('mouseup', function () {
341 painting = false;
342 });
343
344
345 canvas.addEventListener('contextmenu', function (e) { e.preventDefault(); });
346
347
348 playPauseBtn.addEventListener('click', function () {
349 setRunning(!running);
350 });
351
352 stepBtn.addEventListener('click', function () {
353 setRunning(false);
354 advance();
355 draw();
356 });
357
358 clearBtn.addEventListener('click', function () {
359 setRunning(false);
360 grid = Life.makeGrid(COLS, ROWS);
361 generation = 0;
362 draw();
363 });
364
365 randomBtn.addEventListener('click', function () {
366 setRunning(false);
367 grid = Life.makeGrid(COLS, ROWS);
368 for (let r = 0; r < ROWS; r++) {
369 for (let c = 0; c < COLS; c++) {
370 grid[r][c] = Math.random() < 0.28 ? 1 : 0;
371 }
372 }
373 generation = 0;
374 draw();
375 });
376
377 speedInput.addEventListener('input', function () {
378 speed = parseInt(speedInput.value, 10);
379 speedValue.textContent = speed + ' gen/s';
380 });
381
382
383 function setSelectedPattern(key) {
384 selectedPattern = key;
385 hoverCell = null;
386
387 const buttons = patternButtons.querySelectorAll('button');
388 buttons.forEach(function (b) {
389 b.classList.toggle('active', b.dataset.pattern === key);
390 });
391 drawBtn.classList.toggle('active', key === null);
392 canvas.classList.toggle('placing', key !== null);
393 draw();
394 }
395
396 Object.keys(Life.PATTERNS).forEach(function (key) {
397 const btn = document.createElement('button');
398 btn.textContent = Life.PATTERNS[key].name;
399 btn.dataset.pattern = key;
400 btn.addEventListener('click', function () {
401
402 setSelectedPattern(selectedPattern === key ? null : key);
403 });
404 patternButtons.appendChild(btn);
405 });
406
407 drawBtn.addEventListener('click', function () {
408 setSelectedPattern(null);
409 });
410
411
412 speedValue.textContent = speed + ' gen/s';
413 setSelectedPattern(null);
414
415
416 Life.placePattern(grid, Life.PATTERNS.gliderGun.cells, 2, 4, false);
417 draw();
418 }
419
420 if (document.readyState === 'loading') {
421 document.addEventListener('DOMContentLoaded', initUI);
422 } else {
423 initUI();
424 }
425})(typeof globalThis !== 'undefined' ? globalThis : this);
426
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.