Game of Life as a React Component using Canvas
While redesigning my portfolio site I thought it would be cool to integrate the classic programming...
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;
};
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;
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%`;
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.