Game of Life as a React Component using Canvas

Ben Winchester - May 23 '20 - - Dev Community

While redesigning my portfolio site I thought it would be cool to integrate the classic programming simulation, Game of Life by John Conway. It turns out using canvas with React is a bit of a challenge for beginners, and required putting together multiple online resources I found. Here's how to do it.

Conway's Game of Life

The first step is pretty straight forward in regards to programming a canvas to display the Game of Life. For this I followed Adrian Henry's tutorial. I won't rehash the walkthrough here, but I encourage you to watch him and clone the final code from here.

This is what we will convert into a header element.

Let's just drop it into the Component render method!

That isn't possible.

The VanillaJS code requires the canvas element to already be mounted on the DOM so it can be referenced in the first line of the code. Whenever we want something to occur after the element has been mounted, we have to use the useEffect hook (when using functional components). So lets try placing the entire original script inside component's useEffect hook...

Remove logic from the useEffect

This turned out to be very challenging, because we will have to refactor our functions to take dependent variables. Lets start with moving the buildGrid() function out of the component completely, and add the COLS and ROWS as dependent parameters. Move the now buildGrid(COLS,ROWS) function above the GameOfLife component.

The same can be done with the nextGen(grid) function, but because it references elements on the DOM, it must remain in the component, so we can move it out of the useEffect hook, and add the requisite COLS and ROWS parameters like so: nextGen(grid, COLS, ROWS)

Noting has changed on the view yet but we have cleaned up out useEffect hook to make room for the next step: making the canvas and pixel resolution responsive.

Make it fill the width of the screen, and make it responsive

For this section, my primary source was Pete Corey's tutorial.

First thing we need to introduce is the useRef hook to allow us to interact with the canvas. And we will create the getPixelRatio function above the component definition:

const getPixelRatio = context => {
    var backingStore =
    context.backingStorePixelRatio ||
    context.webkitBackingStorePixelRatio ||
    context.mozBackingStorePixelRatio ||
    context.msBackingStorePixelRatio ||
    context.oBackingStorePixelRatio ||
    context.backingStorePixelRatio ||
    1;

    return (window.devicePixelRatio || 1) / backingStore;
};
Enter fullscreen mode Exit fullscreen mode

The getPixelRatio function will return the aspect ratio of the viewport to avoid stretching the pixels and help us keep square colonies in our Game of Life simulation.

Now we can replace

canvas.width = 800;
canvas.height = 800;

Enter fullscreen mode Exit fullscreen mode

with

let ratio = getPixelRatio(ctx);
    let width = getComputedStyle(canvas)
        .getPropertyValue('width')
        .slice(0, -2);
    let height = getComputedStyle(canvas)
        .getPropertyValue('height')
        .slice(0, -2);

    canvas.width = width * ratio;
    canvas.height = height * ratio;
    canvas.style.width = `100%`;
    canvas.style.height = `100%`;
Enter fullscreen mode Exit fullscreen mode

Because of the restricted size of the embedded codepen, I changed the value of resolution to 1 to give a larger field of colonies.

Slow it down!

If you are familiar with Conway's Game of Life, you're aware that a stable state is reach fairly quickly, so lets slow down the rate at which the animation refreshes so it lasts longer. An additional benefit, is it slows down the rate in which images are flashing on the screen for accessibility purposes. To accomplish this, we will need to wrap the requestAnimationFrame(update) call inside the update function in a setTimeout function and give it a 2 second refresh rate, like so: setTimeout(() => requestAnimationFram(update), 2000).

Give it a little style

Now lets add a little color. This must be done in the javascript where the canvas is being drawn. I will add variables to hold the hex values for the fieldColor and colonyColor and change the "black" and "white" options, which are contingent on the grid values, to reference these variables.

Now I want to add a slant to the bottom on the header. Typically, we would use CSS transform's skew function, but that would ruin the grid nature of the colonies. So I applied a simple clip-path to the CSS.

You can see the full application on my portfolio page or view the repo on github.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .