1
2
3export function createGrid(rows, cols, fill = 0) {
4 return Array.from({ length: rows }, () => new Array(cols).fill(fill));
5}
6
7export function cloneGrid(grid) {
8 return grid.map((row) => row.slice());
9}
10
11export function countNeighbors(grid, r, c, wrap = false) {
12 const rows = grid.length;
13 const cols = grid[0].length;
14 let count = 0;
15 for (let dr = -1; dr <= 1; dr++) {
16 for (let dc = -1; dc <= 1; dc++) {
17 if (dr === 0 && dc === 0) continue;
18 let nr = r + dr;
19 let nc = c + dc;
20 if (wrap) {
21 nr = ((nr % rows) + rows) % rows;
22 nc = ((nc % cols) + cols) % cols;
23 } else if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) {
24 continue;
25 }
26 if (grid[nr][nc]) count++;
27 }
28 }
29 return count;
30}
31
32export function nextGeneration(grid, wrap = false) {
33 const rows = grid.length;
34 const cols = grid[0].length;
35 const next = createGrid(rows, cols);
36 for (let r = 0; r < rows; r++) {
37 for (let c = 0; c < cols; c++) {
38 const n = countNeighbors(grid, r, c, wrap);
39 const alive = grid[r][c] === 1;
40 if (alive && (n === 2 || n === 3)) next[r][c] = 1;
41 else if (!alive && n === 3) next[r][c] = 1;
42 }
43 }
44 return next;
45}
46
47function decode(rows) {
48 return rows.map((row) =>
49 row.split('').map((ch) => (ch === 'O' || ch === 'X' || ch === '1' ? 1 : 0)),
50 );
51}
52
53export const PATTERNS = {
54 glider: decode([
55 '.O.',
56 '..O',
57 'OOO',
58 ]),
59 pulsar: decode([
60 '..OOO...OOO..',
61 '.............',
62 'O....O.O....O',
63 'O....O.O....O',
64 'O....O.O....O',
65 '..OOO...OOO..',
66 '.............',
67 '..OOO...OOO..',
68 'O....O.O....O',
69 'O....O.O....O',
70 'O....O.O....O',
71 '.............',
72 '..OOO...OOO..',
73 ]),
74 gliderGun: decode([
75 '........................O...........',
76 '......................O.O...........',
77 '............OO......OO............OO',
78 '...........O...O....OO............OO',
79 'OO........O.....O...OO..............',
80 'OO........O...O.OO....O.O...........',
81 '..........O.....O.......O...........',
82 '...........O...O....................',
83 '............OO......................',
84 ]),
85};
86
87export function placePattern(grid, pattern, r0, c0, wrap = false) {
88 const rows = grid.length;
89 const cols = grid[0].length;
90 for (let r = 0; r < pattern.length; r++) {
91 for (let c = 0; c < pattern[r].length; c++) {
92 if (!pattern[r][c]) continue;
93 let nr = r0 + r;
94 let nc = c0 + c;
95 if (wrap) {
96 nr = ((nr % rows) + rows) % rows;
97 nc = ((nc % cols) + cols) % cols;
98 } else if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) {
99 continue;
100 }
101 grid[nr][nc] = 1;
102 }
103 }
104 return grid;
105}
106
107
108
109if (typeof document !== 'undefined' && typeof window !== 'undefined') {
110 const init = () => {
111 const canvas = document.getElementById('grid');
112 if (!canvas) return;
113 const ctx = canvas.getContext('2d');
114 const CELL = 12;
115 const COLS = Math.floor(canvas.width / CELL);
116 const ROWS = Math.floor(canvas.height / CELL);
117
118 let grid = createGrid(ROWS, COLS);
119 let wrap = false;
120 let running = false;
121 let generation = 0;
122 let fps = 12;
123 let acc = 0;
124 let lastFrame = 0;
125 let selectedPattern = null;
126
127 const $ = (id) => document.getElementById(id);
128 const playBtn = $('play');
129 const stepBtn = $('step');
130 const clearBtn = $('clear');
131 const speedInput = $('speed');
132 const speedLabel = $('speed-label');
133 const wrapInput = $('wrap');
134 const genLabel = $('gen');
135 const patternButtons = document.querySelectorAll('[data-pattern]');
136 const hint = $('hint');
137
138 function draw() {
139 ctx.fillStyle = '#0d1017';
140 ctx.fillRect(0, 0, canvas.width, canvas.height);
141
142 ctx.strokeStyle = '#1c2230';
143 ctx.lineWidth = 1;
144 ctx.beginPath();
145 for (let c = 0; c <= COLS; c++) {
146 ctx.moveTo(c * CELL + 0.5, 0);
147 ctx.lineTo(c * CELL + 0.5, ROWS * CELL);
148 }
149 for (let r = 0; r <= ROWS; r++) {
150 ctx.moveTo(0, r * CELL + 0.5);
151 ctx.lineTo(COLS * CELL, r * CELL + 0.5);
152 }
153 ctx.stroke();
154
155 ctx.fillStyle = '#7dd3fc';
156 for (let r = 0; r < ROWS; r++) {
157 for (let c = 0; c < COLS; c++) {
158 if (grid[r][c]) {
159 ctx.fillRect(c * CELL + 1, r * CELL + 1, CELL - 2, CELL - 2);
160 }
161 }
162 }
163 }
164
165 function updateGen() {
166 genLabel.textContent = String(generation);
167 }
168
169 function step() {
170 grid = nextGeneration(grid, wrap);
171 generation++;
172 updateGen();
173 draw();
174 }
175
176 function loop(t) {
177 if (!running) return;
178 if (!lastFrame) lastFrame = t;
179 acc += (t - lastFrame) / 1000;
180 lastFrame = t;
181 const interval = 1 / fps;
182 while (acc >= interval) {
183 grid = nextGeneration(grid, wrap);
184 generation++;
185 acc -= interval;
186 }
187 updateGen();
188 draw();
189 requestAnimationFrame(loop);
190 }
191
192 function setRunning(next) {
193 running = next;
194 playBtn.textContent = running ? 'Pause' : 'Play';
195 playBtn.classList.toggle('primary', !running);
196 if (running) {
197 acc = 0;
198 lastFrame = 0;
199 requestAnimationFrame(loop);
200 }
201 }
202
203 playBtn.addEventListener('click', () => setRunning(!running));
204 stepBtn.addEventListener('click', () => {
205 if (running) setRunning(false);
206 step();
207 });
208 clearBtn.addEventListener('click', () => {
209 grid = createGrid(ROWS, COLS);
210 generation = 0;
211 updateGen();
212 draw();
213 });
214 speedInput.addEventListener('input', (e) => {
215 fps = parseInt(e.target.value, 10);
216 speedLabel.textContent = `${fps} gen/s`;
217 });
218 wrapInput.addEventListener('change', (e) => {
219 wrap = e.target.checked;
220 });
221
222 function setSelectedPattern(name) {
223 selectedPattern = name;
224 patternButtons.forEach((b) => {
225 b.classList.toggle('active', b.dataset.pattern === selectedPattern);
226 });
227 canvas.classList.toggle('placing', !!selectedPattern);
228 if (hint) {
229 hint.textContent = selectedPattern
230 ? `Click grid to place ${selectedPattern}. Click the pattern again to deselect.`
231 : 'Drag on the grid to draw cells. Pick a pattern to place it by click.';
232 }
233 }
234
235 patternButtons.forEach((btn) => {
236 btn.addEventListener('click', () => {
237 const name = btn.dataset.pattern;
238 setSelectedPattern(name === selectedPattern ? null : name);
239 });
240 });
241
242 let dragging = false;
243 let dragMode = 0;
244 const touched = new Set();
245
246 function cellAt(evt) {
247 const rect = canvas.getBoundingClientRect();
248 const x = ((evt.clientX - rect.left) / rect.width) * canvas.width;
249 const y = ((evt.clientY - rect.top) / rect.height) * canvas.height;
250 return { r: Math.floor(y / CELL), c: Math.floor(x / CELL) };
251 }
252
253 canvas.addEventListener('mousedown', (e) => {
254 const { r, c } = cellAt(e);
255 if (r < 0 || r >= ROWS || c < 0 || c >= COLS) return;
256 if (selectedPattern) {
257 const pat = PATTERNS[selectedPattern];
258 const r0 = r - Math.floor(pat.length / 2);
259 const c0 = c - Math.floor(pat[0].length / 2);
260 placePattern(grid, pat, r0, c0, wrap);
261 draw();
262 return;
263 }
264 dragging = true;
265 dragMode = grid[r][c] ? 0 : 1;
266 grid[r][c] = dragMode;
267 touched.clear();
268 touched.add(`${r},${c}`);
269 draw();
270 });
271
272 canvas.addEventListener('mousemove', (e) => {
273 if (!dragging) return;
274 const { r, c } = cellAt(e);
275 if (r < 0 || r >= ROWS || c < 0 || c >= COLS) return;
276 const key = `${r},${c}`;
277 if (touched.has(key)) return;
278 touched.add(key);
279 grid[r][c] = dragMode;
280 draw();
281 });
282
283 window.addEventListener('mouseup', () => {
284 dragging = false;
285 });
286 canvas.addEventListener('mouseleave', () => {
287 dragging = false;
288 });
289
290 document.addEventListener('keydown', (e) => {
291 if (e.target && ['INPUT', 'TEXTAREA'].includes(e.target.tagName)) return;
292 if (e.key === ' ') {
293 e.preventDefault();
294 setRunning(!running);
295 } else if (e.key === 'n' || e.key === 'ArrowRight') {
296 if (running) setRunning(false);
297 step();
298 } else if (e.key === 'c') {
299 grid = createGrid(ROWS, COLS);
300 generation = 0;
301 updateGen();
302 draw();
303 } else if (e.key === 'Escape') {
304 setSelectedPattern(null);
305 }
306 });
307
308
309 placePattern(grid, PATTERNS.glider, 2, 2);
310 placePattern(grid, PATTERNS.pulsar, 10, 20);
311 updateGen();
312 draw();
313 };
314
315 if (document.readyState === 'loading') {
316 document.addEventListener('DOMContentLoaded', init);
317 } else {
318 init();
319 }
320}
321
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.