<!DOCTYPE html>
<html lang="en-GB">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="description" content="Yeah">
        <title>Jason Cartwright</title>
        <style>
            html, body {
                margin: 0;
                padding: 0;
            }
            body {
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
                font-size: 17px;
                background: #fafafa;
                position: relative;
            }
            canvas {
                position: absolute;
                top: 0;
                left: 0;
                width: 100%;
                z-index: 0;
                display: block;
                pointer-events: none;
            }
            main {
                position: relative;
                z-index: 1;
            }
            p {
                font-size: 143px;
                padding: 5px 0 0 25px;
                margin: 0;
            }
            a {
                color: #0172ad;
            }
            dt {
                margin: 0 0 2px 25px;
            }
            dd {
                margin: 0 0 8px 25px;
            }
            address {
                margin: 30px 0 30px 25px;
            }
            @media (min-width: 805px) {
                dl {
                    column-count: 2;
                    column-width: 390px;
                    column-gap: 0;
                    max-width: 805px;
                }
                dt {
                    break-after: avoid;
                }
                dd {
                    break-before: avoid;
                    break-inside: avoid;
                }
            }
        </style>
        <script async src="https://plausible.io/js/pa-7n5WOgdu4Rrzw32W-nDa3.js"></script>
        <script>
            window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
            plausible.init()
        </script>
    </head>
    <body>
        <main>
            <p>👋</p>
            <dl>
                <dt><a href="https://www.givefood.org.uk">Give Food</a></dt>
                <dd>UK food bank charity</dd>
                <dt><a href="https://crawl.news">crawl.news</a></dt>
                <dd>News aggregator</dd>
                <dt><a href="https://opencharities.uk">OpenCharities</a></dt>
                <dd>Open UK charity data</dd>
                <dt><a href="https://corpdle.com">Corpdle</a></dt>
                <dd>Wordle for S&P 500 companies</dd>
                <dt><a href="https://constructofthe.day">Construct of the Day</a></dt>
                <dd>Daily programming language constructs</dd>
                <dt><a href="https://wi.kifa.st">wi.kifa.st</a></dt>
                <dd>Wicked fast Wikipedia articles</dd>
                <dt><a href="https://reviewofparks.london">London Review Of Parks</a></dt>
                <dd>Apologies to London Review of Books</dd>
                <dt><a href="https://metaweather.com">Metaweather</a></dt>
                <dd>Weather forecast aggregator</dd>
                <dt><a href="https://setpdf.app">SetPDF</a></dt>
                <dd>Musician set PDF creator</dd>
                <dt><a href="https://geojson.page">geojson.page</a></dt>
                <dd>View and filter GeoJSON files</dd>
                <dt><a href="https://jasoncartwright.github.io/dmch/">Daily Mail But With The Comments For Headlines</a></dt>
                <dd>:pointup:</dd>
                <dt><a href="https://reformornot.uk/">Am I Reform Or Not</a></dt>
                <dd>2026 local election special</dd>
                <dt><a href="https://guesscandidatesparty.uk">Guess Candidate's Party</a></dt>
                <dd>On hiatus</dd>
            </dl>
            <address><a href="mailto:mail@jasoncartwright.com">mail@jasoncartwright.com</a></address>
        </main>
        <canvas></canvas>
        <script>
            (function () {
                const canvas = document.querySelector('canvas');
                const ctx = canvas.getContext('2d');
                const CELL = 18;
                const RADIUS = 6;
                let cols, rows, grid, next, trail;

                function seed() {
                    grid = new Uint8Array(cols * rows);
                    next = new Uint8Array(cols * rows);
                    trail = new Float32Array(cols * rows);
                    for (let i = 0; i < grid.length; i++) {
                        grid[i] = Math.random() < 0.30 ? 1 : 0;
                    }
                }

                function resize() {
                    canvas.style.display = 'none';
                    const w = window.innerWidth;
                    const h = Math.max(document.documentElement.scrollHeight, window.innerHeight);
                    canvas.style.display = '';
                    const dpr = window.devicePixelRatio || 1;
                    canvas.width = w * dpr;
                    canvas.height = h * dpr;
                    canvas.style.width = w + 'px';
                    canvas.style.height = h + 'px';
                    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
                    cols = Math.ceil(w / CELL) + 1;
                    rows = Math.ceil(h / CELL) + 1;
                    seed();
                }

                function step() {
                    next.fill(0);
                    for (let y = 0; y < rows; y++) {
                        for (let x = 0; x < cols; x++) {
                            let n = 0;
                            for (let dy = -1; dy <= 1; dy++) {
                                for (let dx = -1; dx <= 1; dx++) {
                                    if (dx === 0 && dy === 0) continue;
                                    const nx = (x + dx + cols) % cols;
                                    const ny = (y + dy + rows) % rows;
                                    n += grid[ny * cols + nx];
                                }
                            }
                            const i = y * cols + x;
                            const alive = grid[i];
                            if (alive && (n === 2 || n === 3)) next[i] = 1;
                            else if (!alive && n === 3) next[i] = 1;
                            if (alive && !next[i]) trail[i] = 1;
                        }
                    }
                    const tmp = grid;
                    grid = next;
                    next = tmp;
                }

                let lastStep = 0;
                let staleTicks = 0;
                let prevPop = 0;

                function render() {
                    ctx.fillStyle = '#fafafa';
                    ctx.fillRect(0, 0, canvas.width, canvas.height);
                    for (let y = 0; y < rows; y++) {
                        for (let x = 0; x < cols; x++) {
                            const i = y * cols + x;
                            const cx = x * CELL + CELL / 2;
                            const cy = y * CELL + CELL / 2;
                            if (grid[i]) {
                                ctx.fillStyle = 'rgba(80, 150, 200, 0.08)';
                                ctx.beginPath();
                                ctx.arc(cx, cy, RADIUS, 0, Math.PI * 2);
                                ctx.fill();
                            } else if (trail[i] > 0.02) {
                                ctx.fillStyle = `rgba(80, 150, 200, ${trail[i] * 0.04})`;
                                ctx.beginPath();
                                ctx.arc(cx, cy, RADIUS * trail[i], 0, Math.PI * 2);
                                ctx.fill();
                                trail[i] *= 0.9;
                            }
                        }
                    }
                }

                function tick() {
                    step();
                    let pop = 0;
                    for (let i = 0; i < grid.length; i++) pop += grid[i];
                    if (Math.abs(pop - prevPop) < 3) staleTicks++;
                    else staleTicks = 0;
                    prevPop = pop;
                    if (staleTicks > 60 || pop < cols * rows * 0.02) {
                        seed();
                        staleTicks = 0;
                    }
                    render();
                }

                function loop(t) {
                    if (t - lastStep > 320) {
                        tick();
                        lastStep = t;
                    }
                    requestAnimationFrame(loop);
                }

                const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');

                function start() {
                    resize();
                    if (reduceMotion.matches) {
                        render();
                    } else {
                        render();
                        requestAnimationFrame(loop);
                    }
                }

                window.addEventListener('resize', () => {
                    resize();
                    render();
                });
                reduceMotion.addEventListener('change', start);
                start();
            })();
        </script>
    </body>
</html>
