Will Hall's

journey through everything.

Simplex noise is often used in game development for the dynamic generation of terrains and landscapes. It works because it creates smooth gradients between all points and is relatively computationally cheap. The noise can be generated to any number of dimensions, but in this arty tutorial will use three dimensions (two for the X-Y plane and one for ⌚ time ⌚ itself) to evolve a colourful dynamic noise pattern on an HTML canvas.

This glowy floaty canvas is what we will be working towards.

For those itching to see the inside magic, take a peek at the above sandbox which has the colourful canvas plus all the possible extensions and fun extras written about in this article. However, if you're looking for the long haul then buckle up; let's take it from scratch.

Configuring our React Environment

First thing first, lets define our environment. Here, I will be using the React framework with Styled Components, but you could also do this with pure HTML+CSS+JS and things would work just fine. To generate the noise we will use the simplex-noise package, so there are a few things to install. To get started, lets run create-react-app:

  1. Make sure you have node and npm installed. Use can use nvm to get started if not.
  2. Make sure you have npx installed. You can run npm i -G npx to install.
  3. Run npx create-react-app colourful-canvas. CRA will work it's magic and the development environment will be sorted.
  4. cd colourful-canvas && npm i -S styled-components simplex-noise.
  5. npm run start will launch the dev server on http://localhost:3000.

Now, let's crack on with our canvas component. Open your IDE and jump into your project directory.

Canvas Component

Getting Started

Canvases are notoriously "quirky" to work with in a React environment since they are usually (and should be) animated upon outside the React rendering lifecycle. First, we need a set reference to our canvas, so we can access and change the canvas DOM node without going through a whole React render cycle. You can think of a reference like a big mutable box that you can put whatever you want in, and crucially it won't trigger a re-render when what's inside it changes. Let's set up the component; in src/ColourfulCanvas.js:

import React, { useRef } from "react";
import styled from "styled-components";

const StyledCanvas = styled.canvas`
  margin: 0;
  padding: 0;
  width: 100px;
  height: 100px;
`;

const ColourfulCanvas = () => {
  const canvasRef = useRef();
  return <StyledCanvas ref={canvasRef} width="100px" height="100px" />;
};

export default ColourfulCanvas;

Here's what just happened:

  1. We import React and the useRef hook. Hooks allow you to 'hook' into React functionality (e.g. references) from within a functional component (e.g. a not an extended class component).
  2. We use Styled Components to style a canvas. Here, we are styling the canvas be 100px by 100px, and to remove the default margins and padding. These are the CSS styled dimensions.
  3. We declare our functional component ColourfulCanvas and return our StyledCanvas. Inside the component, we declare canvasRef using useRef() to hook into React references. We then pass the reference to the canvas in the ref attribute. We must also set the canvas' internal dimensions (how many pixels to paint) in the returned StyledCanvas.

Next, change src/App.js to render our new component:

import React from "react";
import ColourfulCanvas from "./ColourfulCanvas";

function App() {
  return <ColourfulCanvas />;
}

export default App;

Head on over to http://localhost:3000 and you will see... absolutely nothing. The next step is to actually print something on the canvas.

Printing to the Canvas

To print to the canvas, we must access the canvas context from the canvas reference value. Then we can use the context to draw on the canvas.

...
const ColourfulCanvas = () => {
  const canvasRef = useRef();
  const ctx = canvasRef.current.getContext("2d");
  const imageData = ctx.createImageData(100, 100);

  for (let x = 0; x < 100; x++) {
    for (let y = 1; y < 100; y++) {
      const index = (x + y * 100) * 4;
      imageData.data[index] = 255;
      imageData.data[index + 1] = 0;
      imageData.data[index + 2] = 0;
      imageData.data[index + 3] = 255;
    }
  }

  ctx.putImageData(imageData, 0, 0);

  return <StyledCanvas ref={canvasRef} width="100px" height="100px" />;
};
...

Here's what is happening:

  1. We use canvasRef.current to target the canvas DOM node, and call getContext("2d") to access the canvas context. Now, we can draw!
  2. We create an imageData of size 100 by 100. This is just an object with some image metadata, including a big array which holds pixel data.
  3. Calculate the index of the pixel in the array. The index is derived from the X and Y position of the pixel, times by the width of the canvas, times by 4, which is the number of bytes in imageData that represent a single pixel. Each pixel has a byte for red, green, blue and alpha. In integer terms, this is any number between 0 and 255.
  4. Set the RBGA value of each pixel to be 255,0,0,255, otherwise known as red.
  5. Finally, we put the image data onto the canvas at position 0,0, which is the top-left corner of the canvas.

Right, let's run this code! Oh, looks like we get this error:

TypeError: canvasRef.current is undefined

This is because we are trying to reference the canvas before it has actually mounted. To fix this, we will once again have to 'hook' into the React lifecycle and use the useEffect hook to tie into the component's mounting. This hook specifically runs whatever code is inside it after the component is rendered, and so we can be sure that our canvas reference actually exists.

...
const ColourfulCanvas = () => {
  const canvasRef = useRef();
	
  // This UseEffect hook will run any passed function 
  // when the component renders for the first time!
  useEffect(() => {
    
    const ctx = canvasRef.current.getContext("2d");
    const imageData = ctx.createImageData(100, 100);

    for (let x = 0; x < 100; x++) {
      for (let y = 1; y < 100; y++) {
        const index = (x + y * 100) * 4;
        imageData.data[index] = 255;
        imageData.data[index + 1] = 0;
        imageData.data[index + 2] = 0;
        imageData.data[index + 3] = 255;
      }
    }

    ctx.putImageData(imageData, 0, 0);

  },[])
  

  return <StyledCanvas ref={canvasRef} width="100px" height="100px" />;
};
...

The empty array ([]) in the second argument of useEffect is an array of dependencies. If this array is empty, then React assumes that there are no stateful values (props or internal state) used within the effect that, when changed, would trigger a re-render of the component. This means that the effect will only run once when the component mounts (aka. first render), and then never again. If you were to omit the array completely, then the opposite behaviour would take place; the effect would run on every render.

Finally, if there were some stateful values that were used inside the effect, then you would pass those values in the array. Remember, any stateful values used inside the effect is essentially 'snapshotted' when it is first run e.g. on mount. If you don't add the stateful values into the dependancy array, then any logic you perform in the effect might reference stale values.

Having a dependancy array means the useEffect hook knows when it needs to re-run with fresh stateful values, and take a new 'snapshot'.

Let's get Noisy

Now, lets introduce some simplex noise!

import SimplexNoise from "simplex-noise";
...
const ColourfulCanvas = () => {
  const canvasRef = useRef();
  const simplex = useRef(new SimplexNoise());

  useEffect(() => {
    const ctx = canvasRef.current.getContext("2d");
    const imageData = ctx.createImageData(100, 100);

    for (let x = 0; x < 100; x++) {
      for (let y = 0; y < 100; y++) {
        const index = (x + y * 100) * 4;
        const noise = simplex.current.noise3D(x, y, 0);
        imageData.data[index] = noise * 255;
        imageData.data[index + 1] = noise * 255;
        imageData.data[index + 2] = noise * 255;
        imageData.data[index + 3] = 255;
      }
    }

    ctx.putImageData(imageData, 0, 0);
  }, []);

  return <StyledCanvas ref={canvasRef} width="100px" height="100px" />;
};
...

Let's look at what has changed:

  1. We import simplex-noise at the top of the document.
  2. We declare an instance of SimplexNoise and bung in into a reference. Remember, a ref is just a mutable box we can put anything in when we don't need to worry about re-rendering.
  3. For each pixel, we calculate the value of the noise at this position. Using noise3D, the first two dimensions are the X and Y plane, and the final dimension is time. We will have fun with time later on.

Adding Colours

The result is a grainy-looking canvas, with black and white dots everywhere. Not very interesting. To make things a little more pretty, we will use the noise value to interpolate between two RGB values, so giving us some colour:

...
const lerp = (x, x1, x2, y1, y2) => y1 + (x - x1) * ((y2 - y1) / (x2 - x1));

const getPixel = (noise, palette) => {
  let rgb = [];

  for (let i = 0; i < 3; i++) {
    rgb.push(lerp(Math.abs(noise), 0, 1, palette[0][i], palette[1][i]));
  }
  return rgb;
};

const palette = [
  [255, 129, 0],
  [255, 0, 114],
];
...
  1. First we defined a functional expression called lerp. You can read more about linear interpolation here, but for now, imagine it as using the magnitude of one value to scale between two other values.
  2. Next we defined a functional expression called getPixel. Get pixel uses lerp and the magnitude of the noise value to scale the between red, green and blue pixel values defined in the palette array. Each pixel is returned as an array.
...
    for (let x = 0; x < 100; x++) {
      for (let y = 0; y < 100; y++) {
        const index = (x + y * 100) * 4;
        const noise = simplex.current.noise3D(x, y, 0);
        const pixel = getPixel(noise, palette);
        imageData.data[index] = pixel[0];
        imageData.data[index + 1] = pixel[1];
        imageData.data[index + 2] = pixel[2];
        imageData.data[index + 3] = 255;
      }
    }
...
  1. Finally we update the loop in our effect. Now we calculate the pixel value using our new getPixel function, injecting a burst of colour into the noise.

The result is something a little nicer, but still grainy. To fix this, let's scale down the noise dimensions by a factor of 100.

...
const noise = simplex.current.noise3D(x / 100, y / 100, 0);
...

Wibbly Wobbly Timey Wimey

Now, let's evolve the noise over time to give that organic, flowing effect. To accomplish this, we will lift the current logic inside useEffect into a discreet functional expression, and then use requestAnimationFrame to call that function.

...
const ColourfulCanvas = () => {
  const canvasRef = useRef();
  const simplex = useRef(new SimplexNoise());
  const tRef = useRef(0);
  const rafRef = useRef();

  useEffect(() => {
    const frame = () => {
      const ctx = canvasRef.current.getContext("2d");
      const imageData = ctx.createImageData(100, 100);

      for (let x = 0; x < 100; x++) {
        for (let y = 0; y < 100; y++) {
          const index = (x + y * 100) * 4;
          const noise = simplex.current.noise3D(x / 100, y / 100, tRef.current);
          const pixel = getPixel(noise, palette);
          imageData.data[index] = pixel[0];
          imageData.data[index + 1] = pixel[1];
          imageData.data[index + 2] = pixel[2];
          imageData.data[index + 3] = 255;
        }
      }

      ctx.putImageData(imageData, 0, 0);
      tRef.current++;
      rafRef.current = requestAnimationFrame(frame);
    };

    rafRef.current = requestAnimationFrame(frame);

    return () => cancelAnimationFrame(rafRef.current);
  }, []);

  return <StyledCanvas ref={canvasRef} width="100px" height="100px" />;
};
...

OK, a lot has changed. Let's step through it:

  1. We declared two references: One for time and one for requestAnimationFrame, called tRef and rafRef respectively. Just another set of mutable boxes.
  2. We wrapped our entire canvas painting logic in the frame functional expression.
  3. We pass tRef.current into the third dimension of the noise function.
  4. At the end of every frame call, we increment the time reference by 1. Now the noise will evolve over time.
  5. At the end of every frame call, we also request the next frame, and store a reference to this request just in case we need to cancel it. (This becomes handy if you need to dynamically pause / play the animation, or unmount the component).
  6. At the end of the effect, we request the first animation frame and store a reference to the request. This starts the animation loop.
  7. At the end of the effect, we also return a 'clean up' function cancelAnimationFrame(rafRef.current). This will run when the component unmounts. If the canvas disappears, we want to halt animation.

Now take a look at the canvas. Right now, this looks anything BUT calming and organic... That is because we are evolving the noise by too greater value with each frame. To fix this, let's scale down the third dimension of our noise:

...
const noise = simplex.current.noise3D(x / 100, y / 100, tRef.current / 750);
...

Ah! Finally, we have something which could pass as an organic evolving colourful canvas. Now you can play with all the scaling values of the noise function to get something that works for you.

From this point, you can go ahead and throw it in anywhere you like. Altering the styling can give you plenty of options (see below for some further examples and extensions). Use it as a a header, in a fun div, or an entire site background (*cough self-promotion cough*).

Full Code

import React, { useRef, useEffect } from "react";
import styled from "styled-components";
import SimplexNoise from "simplex-noise";

const StyledCanvas = styled.canvas`
  margin: 0;
  padding: 0;
  width: 100px;
  height: 100px;
`;

const lerp = (x, x1, x2, y1, y2) => y1 + (x - x1) * ((y2 - y1) / (x2 - x1));

const getPixel = (noise, palette) => {
  let rgb = [];

  for (let i = 0; i < 3; i++) {
    rgb.push(lerp(Math.abs(noise), 0, 1, palette[0][i], palette[1][i]));
  }
  return rgb;
};

const palette = [
  [255, 129, 0],
  [255, 0, 114],
];

const ColourfulCanvas = () => {
  const canvasRef = useRef();
  const simplex = useRef(new SimplexNoise());
  const tRef = useRef(0);
  const rafRef = useRef();

  useEffect(() => {
    const frame = () => {
      const ctx = canvasRef.current.getContext("2d");
      const imageData = ctx.createImageData(100, 100);

      for (let x = 0; x < 100; x++) {
        for (let y = 0; y < 100; y++) {
          const index = (x + y * 100) * 4;
          const noise = simplex.current.noise3D(
            x / 100,
            y / 100,
            tRef.current / 750
          );
          const pixel = getPixel(noise, palette);
          imageData.data[index] = pixel[0];
          imageData.data[index + 1] = pixel[1];
          imageData.data[index + 2] = pixel[2];
          imageData.data[index + 3] = 255;
        }
      }

      ctx.putImageData(imageData, 0, 0);
      tRef.current++;
      rafRef.current = requestAnimationFrame(frame);
    };

    rafRef.current = requestAnimationFrame(frame);

    return () => cancelAnimationFrame(rafRef.current);
  }, []);

  return <StyledCanvas ref={canvasRef} width="100px" height="100px" />;
};

export default ColourfulCanvas;

Examples and Extensions

Dynamic Props

Suppose you wanted to make the canvas more dynamic; and by that, I mean controlling width, height, colours, speed, noise detail and more. Because the simplex noise object itself is held in a ref outside of the effect hook, we can pass in and change any props (causing a React re-render), and the simplex noise itself will continue to evolve along the same path without any jump or skitter, even though the canvas elements itself is getting re-rendered in the DOM tree. Cool, right!

We're going to go full ham and pass in a whole darn bunch of properties, all of which you can control and pass in from a parent component!

import React, { useRef, useEffect } from "react";
import styled from "styled-components";
import SimplexNoise from "simplex-noise";

/**
 * Use the styled width and height to give the
 * final CSS size of the canvas.
 */
const StyledCanvas = styled.canvas`
  margin: 0;
  padding: 0;
  width: ${(props) => props.styledWidth}px;
  height: ${(props) => props.styledHeight}px;
`;

const lerp = (x, x1, x2, y1, y2) => y1 + (x - x1) * ((y2 - y1) / (x2 - x1));

const getPixel = (noise, palette) => {
  let rgb = [];

  for (let i = 0; i < 3; i++) {
    rgb.push(lerp(Math.abs(noise), 0, 1, palette[0][i], palette[1][i]));
  }
  return rgb;
};

const ColourfulCanvas = ({
  width,
  height,
  palette,
  speed,
  scale,
  resolution,
  animate,
}) => {
  const canvasRef = useRef();
  const simplex = useRef(new SimplexNoise());
  const tRef = useRef(0);
  const rafRef = useRef();

  useEffect(() => {
    const frame = () => {
      const ctx = canvasRef.current.getContext("2d");
      /**
       * The resolution of the canvas is now defined by a prop!
       */
      const imageData = ctx.createImageData(resolution, resolution);

      /**
       * Now we iterate over the width and height, defined by the resolution prop!
       */
      for (let x = 0; x < resolution; x++) {
        for (let y = 0; y < resolution; y++) {
          const index = (x + y * resolution) * 4;
          /**
           * Now the scale (how "zoomed in" the simplex noise is) is defined in prop!
           * Similarly, the speed of evolution is all a prop.
           */
          const noise = simplex.current.noise3D(
            x / scale,
            y / scale,
            tRef.current / (1000 / speed)
          );
          const pixel = getPixel(noise, palette);
          imageData.data[index] = pixel[0];
          imageData.data[index + 1] = pixel[1];
          imageData.data[index + 2] = pixel[2];
          imageData.data[index + 3] = 255;
        }
      }

      ctx.putImageData(imageData, 0, 0);
      tRef.current++;
      rafRef.current = requestAnimationFrame(frame);
    };

    /**
     * This 'if' block will determine if the noise should animate.
     * 'If' not (pun), then we can cancel any un-fulfilled animation
     * request still in progress (just in case).
     */
    if (animate) {
      rafRef.current = requestAnimationFrame(frame);
    } else {
      cancelAnimationFrame(rafRef.current);
    }

    return () => cancelAnimationFrame(rafRef.current);

    /**
     * Remember, this is all within an effect. These dependencies must be
     * listed here, such that the `frame` function will have the updated props
     * to reference.
     */
  }, [palette, speed, scale, resolution, animate]);

  /**
   * Styled width and height define the actual CSS dimension,
   * the width and height are the canvas internal size. Here, we
   * 'stretch' the canvas out to be the CSS dimensions.
   */
  return (
    <StyledCanvas
      ref={canvasRef}
      styledWidth={width}
      styledHeight={height}
      width={`${resolution}px`}
      height={`${resolution}px`}
    />
  );
};

export default ColourfulCanvas;

Apologies for dropping the whole code here, but there are lots of things which have changed. Take a look at the comments to see what's new! A great way to use this would be to have some sort of window resize handler in the parent and dynamically pass the dimensions down through props! Super-dooper.

Add A Fade-In

Between landing on the page and the first colourful paint to canvas, there is a split second where the bare HTML body shows through as all the mounting and simplex object instantiation faf is going on. To fix this, we could add a fade in effect to somewhat hide the transition, and give a nice entrance to the star of the show.

...
let framePalette = [[], []];

if (tRef.current <= 100) {
  for (let i = 0; i < 2; i++) {
    for (let j = 0; j < 3; j++) {
      framePalette[i][j] = lerp(tRef.current, 0, 100, 255, palette[i][j]);
    }
  }
} else {
  framePalette = [...palette];
}
...
const pixel = getPixel(noise, framePalette);
...

Here we are simply interpolating between white (255 in RGB pixel value talk) and the actual colour as defined by the palette. After we are past the first 100 frames, then just set that frame's palette to be an immutable shallow copy of the base palette.

Adding Text

One of the coolest CSS properties I've discovered recently is mix-blend-mode which, just like in any image editing software, acts as the blending mode between layers. Now, you could just add some plain old white text over top of the canvas. But how fun would that be? Instead, we will add the mix-blend-mode: difference; rule to have a perfectly inverted chunk of words.

In a new file called Title.js, add the following:

import React from "react";
import styled from "styled-components";

const Container = styled.div`
  display: flex;
  align-items: center;
  justify-content: center;
  position: absolute;
`;

const Text = styled.div`
  font-size: 2rem;
  z-index: 99;
  user-select: none;
  color: white;
  mix-blend-mode: difference;
`;

const Title = ({ text }) => {
  return (
    <Container>
      <Text>{text}</Text>
    </Container>
  );
};

export default Title;

There is no real concept of 'layers' per-se in HTML markup or CSS, but you can easily replicate this with the position and z-index attributes shown above. Finally, in App.js, add in our new component to see the changes. Be sure to wrap both components in a fragment (<></>) to ensure only a single child is returned.  

import React from "react";
import ColourfulCanvas from "./ColourfulCanvas";
import Title from "./Title";

function App() {
  return (
    <>
      <Title text={"Hello World!"} />
      <ColourfulCanvas
        width={250}
        height={500}
        palette={[
          [255, 129, 0],
          [255, 0, 114],
        ]}
        speed={5}
        scale={50}
        resolution={50}
        animate={true}
      />
    </>
  );
}

export default App;

Now our text is positioned over the canvas and it looks g r o o v y. Play around with both the CSS positioning of the canvas the text to dial in your desired effect (for example, the code sandbox at the very top of the article. Happy scrolling!).

Evolving Between Pairs of Colours

Suppose you wanted to have not just two colours in motion, but indeed four. Well, let's dive into it, and use just a tad more maths along the way. One of the neat properties of one of our old maths buddies Sine wave is the ability to take a linear value and convert it into a cyclic value. In this case, we will use the power of Sine to transform an input of time into a cycle between two colours. To vary the speed of the transition, you could divide the time parameter used in the Sine calculation by some value, e.g. 10.

...
/**
 * Pass in a time value to our get pixel functional expression.
 */
const getPixel = (noise, time, palette) => {
  let rgb = [];

  /**
   * Use our good buddy `Sine` to convert the linear time
   * into a cyclic value between -1 and 1. Here, just to show my working,
   * I use the mod 360 of time to get a value between 0 and 360 degrees,
   * and then convert to radians for the `Math.sin` function to understand.
   */
  const paletteEvolution = Math.sin(((time / 10) % 360) * (Math.PI / 180));

  /**
   * Now we also interpolate between sister pair members over time
   * as well as between the two members of a single pair by noise.
   */
  for (let i = 0; i < 3; i++) {
    rgb.push(
      lerp(
        Math.abs(noise),
        0,
        1,
        lerp(paletteEvolution, -1, 1, palette[0][0][i], palette[1][0][i]),
        lerp(paletteEvolution, -1, 1, palette[0][1][i], palette[1][1][i])
      )
    );
  }
  return rgb;
};
...
/**
* Pass in the t-ref inside the effect to get things working.
*/
const pixel = getPixel(noise, tRef.current, framePalette);
...

Now, you will need a pass in a palette array with a new shape. An array of arrays of arrays to be exact, but really, this just looks like two pairs of RGB values. You could define the palette as a constant within the component, or you could pass it in as props. If you implemented the dynamic props above, the instance of the colourful canvas in App.js might end up looking a bit like this:

<ColourfulCanvas
  width={250}
  height={500}
  palette={[
   [
      [255, 129, 0],
      [255, 0, 114],
   ],
   [
      [0, 0, 0],
      [255, 255, 255],
   ],
  ]}
  speed={5}
  scale={50}
  resolution={50}
  animate={true}
/>

Finally, if you implemented the fade-in effect, then some changes will have to be made in the logic to accommodate the different palette array shape. Just add another level of iteration to account for the extra 'pair' of pairs:

...
      let framePalette = [
        [[], []],
        [[], []],
      ];

      if (tRef.current <= 100) {
        for (let i = 0; i < 2; i++) {
          for (let j = 0; j < 2; j++) {
            for (let k = 0; k < 3; k++) {
              framePalette[i][j][k] = lerp(
                tRef.current,
                0,
                100,
                0,
                palette[i][j][k]
              );
            }
          }
        }
      } else {
        framePalette = [...palette];
      }
...
Oh, Hi! Welcome to the end. Here is a tune to celebrate. I hope you don't mind. 
you might like

© Will Hall 2020