genuary2026/02/src/index.md
2026-01-03 08:22:58 +11:00

3.3 KiB

Genuary26 - 2

Prompt: Twelve principles of animation

Conway's Game of Life jazzed up with some d3 transitions.

Mike Lynch


const WIDTH = 800;
const HEIGHT = 600;

const CELL = 10;
const CELLW = 80;
const CELLH = 60;
const GEN_TIME = 1000;
const INIT_PROB =  0.5;

const restart = { restart: false };

const trigger = view(Inputs.button("Restart"));
trigger;
restart["restart"] = true;


function neighbours(i, j, id) {
    const VWRAP = CELLW * (CELLH - 1);
    const e = i > 0 ? id - 1 : id + CELLW - 1;
    const w = i < CELLW - 1 ? id + 1 : id - CELLW + 1;
    const n = [e, id, w].map((v) => j > 0 ? v - CELLW : v + VWRAP);
    const s = [e, id, w].map((v) => j < CELLH - 1 ? v + CELLW : v - VWRAP);
    return [...n, e, w, ...s];
}


const show_grid = (async function* () {
    const grid = [];

    for( let j = 0; j < CELLH; j++ ) {
        for ( let i = 0; i < CELLW; i++ ) {
            const id = j * CELLW + i;
            const cell = { id: id, x: (i + 0.5) * CELL, y: (j + 0.5) * CELL, live: Math.random() > INIT_PROB };
            cell["n"] = neighbours(i, j, id);
            grid.push(cell);
        }
    }


    let i = 0;
    while ( true ) {
        if( restart["restart"] ) {
            grid.forEach((c) => c.live = Math.random() > INIT_PROB);
            restart["restart"] = false
        }
        yield grid.filter((d) => d.live);
        i++;
        const ngrid = [];
        for( const id in grid ) {
            const live_n = grid[id].n.filter((i) => grid[i].live).length;
            if( grid[id].live ) {
                ngrid[id] = ( live_n > 2 && live_n < 4 );
            } else {
                ngrid[id] = ( live_n === 3 );
            }
        }
        for( const id in grid ) {
            grid[id].live = ngrid[id];
        }
        await new Promise((resolve) => setTimeout(resolve, GEN_TIME));
   }
})();



// Set up the svg canvas 

const svg = d3.create("svg")
  .attr("width", WIDTH)
  .attr("height", HEIGHT)
  .attr("viewBox", [ 0, 0, WIDTH, HEIGHT ]);


svg.append("clipPath")
    .attr("id", "clipRect")
    .append("rect")
    .attr("x", 0)
    .attr("y", 0)
    .attr("width", WIDTH)
    .attr("height", HEIGHT);


const bg_g = svg.append("g")
  .attr("id", "background");

bg_g.selectAll("rect")
  .data( [ { bg: "white" } ] )
  .join("rect")
  .attr("x", 0)
  .attr("y", 0)
  .attr("width", WIDTH)
  .attr("height", HEIGHT)
  .attr("fill", (d) => d.bg)
;

const cells_g = svg.append("g")
  .attr("id", "cells");
//  .attr("clip-path", "url(#clipRect)");

display(svg.node());

const ease = d3.easeElastic.period(0.4).amplitude(3);

cells_g.selectAll("circle")
    .data(show_grid, d => d.id)
    .join(
        enter => enter.append("circle")
           .attr("cx", (d) => d.x)
            .attr("cy", (d) => d.y)
            .attr("fill", "green")
              .transition()
              .ease(ease)
            .duration(1000)
           .attr("r", (d) => 10),
        update => update.attr("fill", "blue"),
        exit => exit
            .transition()
            .duration(2000)
            .attr("r", 0)
            .attr("cy", (d) => d.y + 400)
            .remove()
        );