diff --git a/_pages/dropdown.md b/_pages/dropdown.md
index d10c1b5..6ecf863 100644
--- a/_pages/dropdown.md
+++ b/_pages/dropdown.md
@@ -7,6 +7,8 @@ dropdown: true
children:
- title: bookshelf
permalink: /books/
+ - title: life
+ permalink: /life/
- title: divider
- title: blog
permalink: /blog/
diff --git a/_pages/life.md b/_pages/life.md
new file mode 100644
index 0000000..8de88cb
--- /dev/null
+++ b/_pages/life.md
@@ -0,0 +1,39 @@
+---
+layout: page
+title: life
+permalink: /life/
+description: "cellular automata, right here, right now"
+nav: false
+---
+
+Conway's **Game of Life** — a zero-player cellular automaton where cells live, die, and evolve based on four simple rules. It's one of those ideas that looks like a toy but touches computation, emergence, and the edge of chaos. I've been fascinated by it for years. This one seeds itself from my site data — travel coordinates, blog posts, projects. Press **Start** and watch what grows.
+
+
+
+
+
+
+
+ gen 0
+
+
+
+
+
+
+
+**controls:** Space toggle · C clear · R randomize · click toggle cell
diff --git a/assets/js/game-of-life.js b/assets/js/game-of-life.js
new file mode 100644
index 0000000..43ef943
--- /dev/null
+++ b/assets/js/game-of-life.js
@@ -0,0 +1,312 @@
+/**
+ * Conway's Game of Life — seeded from Arnav's site data.
+ * Vanilla JS, Canvas API, zero dependencies.
+ */
+(function () {
+ 'use strict';
+
+ // ── Config ───────────────────────────────────────────────────────────────
+ var COLS = 60;
+ var ROWS = 40;
+ var CELL = 12; // px
+ var DEFAULT_FPS = 8;
+
+ // ── DOM refs ─────────────────────────────────────────────────────────────
+ var canvas = document.getElementById('gol-canvas');
+ if (!canvas) { console.error('[gol] canvas not found'); return; }
+ var ctx = canvas.getContext('2d');
+ var toggleBtn = document.getElementById('gol-toggle');
+ var clearBtn = document.getElementById('gol-clear');
+ var randomBtn = document.getElementById('gol-random');
+ var speedInput = document.getElementById('gol-speed');
+ var speedVal = document.getElementById('gol-speed-val');
+ var genDisplay = document.getElementById('gol-gen');
+
+ canvas.width = COLS * CELL;
+ canvas.height = ROWS * CELL;
+
+ // ── State ────────────────────────────────────────────────────────────────
+ var grid = [];
+ var age = []; // parallel array: how many generations each cell has lived
+ var running = false;
+ var generation = 0;
+ var fps = DEFAULT_FPS;
+ var rafId = null;
+ var lastFrame = 0;
+
+ // ── CSS theme colours ──────────────────────────────────────────────────
+ function getCSSVar(name) {
+ return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
+ }
+ function readTheme() {
+ return {
+ bg: getCSSVar('--global-bg-color') || '#ffffff',
+ text: getCSSVar('--global-text-color') || '#333333',
+ theme: getCSSVar('--global-theme-color') || '#0066cc',
+ };
+ }
+
+ // ── Grid helpers ───────────────────────────────────────────────────────
+ function createGrid(fillFn) {
+ var g = [];
+ var a = [];
+ for (var y = 0; y < ROWS; y++) {
+ g[y] = [];
+ a[y] = [];
+ for (var x = 0; x < COLS; x++) {
+ g[y][x] = fillFn ? fillFn(x, y) : 0;
+ a[y][x] = 0;
+ }
+ }
+ return { grid: g, age: a };
+ }
+
+ function randomFill() { return Math.random() < 0.15 ? 1 : 0; }
+ function emptyFill() { return 0; }
+
+ function resetState(fillFn) {
+ var s = createGrid(fillFn || emptyFill);
+ grid = s.grid;
+ age = s.age;
+ generation = 0;
+ if (genDisplay) genDisplay.textContent = 'gen 0';
+ }
+
+ // ── Seeding from site data ─────────────────────────────────────────────
+ function seedFromData() {
+ resetState(emptyFill);
+ var seed = window.__lifeSeed;
+ if (!seed) {
+ // Fallback: just random
+ resetState(randomFill);
+ return;
+ }
+ // Map travel coordinates to grid cells
+ if (seed.travelCoords && seed.travelCoords.length) {
+ seed.travelCoords.forEach(function (t) {
+ // lng: -180..180 → x: 0..COLS-1
+ var x = Math.round(((t.lng + 180) / 360) * (COLS - 1));
+ // lat: -90..90 → y: 0..ROWS-1 (invert so north is top)
+ var y = Math.round(((90 - t.lat) / 180) * (ROWS - 1));
+ x = Math.max(0, Math.min(COLS - 1, x));
+ y = Math.max(0, Math.min(ROWS - 1, y));
+ grid[y][x] = 1;
+ age[y][x] = 1;
+ });
+ }
+ // Scatter some additional random cells
+ for (var y = 0; y < ROWS; y++) {
+ for (var x = 0; x < COLS; x++) {
+ if (grid[y][x] === 0 && Math.random() < 0.10) {
+ grid[y][x] = 1;
+ age[y][x] = 1;
+ }
+ }
+ }
+ }
+
+ // ── Game logic (toroidal / wrap-around) ────────────────────────────────
+ function countNeighbors(g, x, y) {
+ var count = 0;
+ for (var dy = -1; dy <= 1; dy++) {
+ for (var dx = -1; dx <= 1; dx++) {
+ if (dx === 0 && dy === 0) continue;
+ var nx = (x + dx + COLS) % COLS;
+ var ny = (y + dy + ROWS) % ROWS;
+ count += g[ny][nx];
+ }
+ }
+ return count;
+ }
+
+ function step() {
+ var newGrid = [];
+ var newAge = [];
+ for (var y = 0; y < ROWS; y++) {
+ newGrid[y] = [];
+ newAge[y] = [];
+ for (var x = 0; x < COLS; x++) {
+ var n = countNeighbors(grid, x, y);
+ var alive = grid[y][x];
+ var next = 0;
+ var a = 0;
+ if (alive) {
+ if (n === 2 || n === 3) {
+ next = 1;
+ a = age[y][x] + 1;
+ }
+ } else {
+ if (n === 3) {
+ next = 1;
+ a = 1;
+ }
+ }
+ newGrid[y][x] = next;
+ newAge[y][x] = a;
+ }
+ }
+ grid = newGrid;
+ age = newAge;
+ generation++;
+ if (genDisplay) genDisplay.textContent = 'gen ' + generation;
+ }
+
+ // ── Rendering ──────────────────────────────────────────────────────────
+ function draw() {
+ var theme = readTheme();
+ var w = canvas.width;
+ var h = canvas.height;
+
+ // Background
+ ctx.fillStyle = theme.bg;
+ ctx.fillRect(0, 0, w, h);
+
+ // Grid lines (subtle)
+ ctx.strokeStyle = theme.text;
+ ctx.globalAlpha = 0.06;
+ ctx.lineWidth = 0.5;
+ for (var x = 0; x <= COLS; x++) {
+ ctx.beginPath();
+ ctx.moveTo(x * CELL, 0);
+ ctx.lineTo(x * CELL, h);
+ ctx.stroke();
+ }
+ for (var y = 0; y <= ROWS; y++) {
+ ctx.beginPath();
+ ctx.moveTo(0, y * CELL);
+ ctx.lineTo(w, y * CELL);
+ ctx.stroke();
+ }
+ ctx.globalAlpha = 1;
+
+ // Cells
+ for (var cy = 0; cy < ROWS; cy++) {
+ for (var cx = 0; cx < COLS; cx++) {
+ if (!grid[cy][cx]) continue;
+ var cellAge = age[cy][cx] || 1;
+ // Newborn cells are brighter, older cells slightly dimmer
+ var brightness = Math.max(0.5, 1 - (cellAge - 1) * 0.008);
+ ctx.globalAlpha = brightness;
+ ctx.shadowColor = theme.theme;
+ ctx.shadowBlur = 3;
+
+ // Parse the theme colour to apply brightness
+ ctx.fillStyle = theme.theme;
+
+ var pad = 1;
+ ctx.fillRect(cx * CELL + pad, cy * CELL + pad, CELL - pad * 2, CELL - pad * 2);
+ }
+ }
+ ctx.shadowBlur = 0;
+ ctx.globalAlpha = 1;
+ }
+
+ // ── Game loop ──────────────────────────────────────────────────────────
+ function loop(timestamp) {
+ if (!running) return;
+ var interval = 1000 / fps;
+ if (timestamp - lastFrame >= interval) {
+ step();
+ draw();
+ lastFrame = timestamp;
+ }
+ rafId = requestAnimationFrame(loop);
+ }
+
+ function start() {
+ if (running) return;
+ running = true;
+ lastFrame = performance.now();
+ if (toggleBtn) toggleBtn.textContent = '\u23F8 Pause';
+ rafId = requestAnimationFrame(loop);
+ }
+
+ function pause() {
+ running = false;
+ if (toggleBtn) toggleBtn.textContent = '\u25B6 Start';
+ if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
+ }
+
+ function toggle() {
+ if (running) pause(); else start();
+ }
+
+ // ── Controls ───────────────────────────────────────────────────────────
+ function handleToggle() { toggle(); }
+
+ function handleClear() {
+ var wasRunning = running;
+ if (wasRunning) pause();
+ resetState(emptyFill);
+ draw();
+ }
+
+ function handleRandom() {
+ var wasRunning = running;
+ if (wasRunning) pause();
+ seedFromData();
+ draw();
+ }
+
+ function handleSpeedChange() {
+ fps = parseInt(speedInput.value, 10);
+ if (speedVal) speedVal.textContent = fps;
+ }
+
+ // ── Canvas click: toggle cell ─────────────────────────────────────────
+ function handleCanvasClick(e) {
+ var rect = canvas.getBoundingClientRect();
+ var scaleX = canvas.width / rect.width;
+ var scaleY = canvas.height / rect.height;
+ var mx = (e.clientX - rect.left) * scaleX;
+ var my = (e.clientY - rect.top) * scaleY;
+ var x = Math.floor(mx / CELL);
+ var y = Math.floor(my / CELL);
+ if (x < 0 || x >= COLS || y < 0 || y >= ROWS) return;
+ grid[y][x] = grid[y][x] ? 0 : 1;
+ age[y][x] = grid[y][x] ? 1 : 0;
+ draw();
+ }
+
+ // ── Keyboard shortcuts ────────────────────────────────────────────────
+ function handleKeydown(e) {
+ // Don't intercept when typing in inputs
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
+ if (e.key === ' ' || e.key === 'Spacebar') {
+ e.preventDefault();
+ toggle();
+ } else if (e.key === 'c' || e.key === 'C') {
+ handleClear();
+ } else if (e.key === 'r' || e.key === 'R') {
+ handleRandom();
+ }
+ }
+
+ // ── Init ───────────────────────────────────────────────────────────────
+ function init() {
+ seedFromData();
+ draw();
+
+ if (toggleBtn) toggleBtn.addEventListener('click', handleToggle);
+ if (clearBtn) clearBtn.addEventListener('click', handleClear);
+ if (randomBtn) randomBtn.addEventListener('click', handleRandom);
+ if (speedInput) speedInput.addEventListener('input', handleSpeedChange);
+ canvas.addEventListener('click', handleCanvasClick);
+ document.addEventListener('keydown', handleKeydown);
+
+ // Re-draw on theme change (dark/light toggle)
+ var themeObserver = new MutationObserver(function () {
+ draw();
+ });
+ themeObserver.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ['data-theme'],
+ });
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+})();