Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
September 25, 2021 12:10 pm GMT

Let's build a rainbow on a canvas from scratch!

It's raining since a few days at my place. And even though it actually just stopped raining as I'm writing this post, the sun hardly comes out anymore. It's autumn on the northern hemisphere. The chances of seeing what is probably nature's most colourful phenomenon this year are close to zero. What a pity.

But there's a remedy: Let's just build our own rainbow with JavaScript, some HTML and some mathematics! And no, we're not using any built-in linear gradient functions or CSS today.

But first, I'd like to thank @doekenorg for supporting me via Buy Me A Coffee! Your support is highly appreciated and the coffee was delicious, just the right thing on a cold autumn morning! Thank you!

No built-in linear gradient? How are we going to do this?

With mathematics and a color scheme called HLS. With a few parameters, namely the width and height of the canvas, the angle of the rainbow, which color to start with and which color to end with, we can construct an algorithm that will tell us the exact color of every pixel.

The nice thing: We can also do other things than painting with the result. For example coloring a monospaced text in a rainbow pattern!

HLS? What's that?

Good question! Most people that worked with CSS have seen RGB values before. RGB stands for "Red, Green, Blue". All colors are mixed by telling the machine the amount of red, green and blue. This is an additive color model (all colors together end of in white), red green and yellow on the other hand, is a subtractive color model (all colors together end up black).

HLS is a bit different. Instead of setting the amount of different colors, we describe the color on a cylinder. HLS stands for "hue,lightness, saturation":

HLS color cylinder. the bottom part is black, the top part is white, the colors wrap around the cylinder.

(Image by Wikimedia user SharkD, released under the CC BY-SA 3.0, no changes made to the image)

The lightness determines how bright the color is. 0% always means black, 100% means white. The saturation describes how intense the color is. 0% would mean gray-scale, 100% means the colors are very rich. This image I found on Stackoverflow describes it very well:

Two color gradients describing lightness and saturation

Now, the hue part is what's interesting to us. It describes the actual color on a scale from 0 degrees to 360 degrees. For better understanding, the Stackoverflow post I mentioned above also has a very nice illustration for that:

Color wheel with angles

If we want to make a rainbow with HLS, we set the colors as always mid-brightness (not black nor white), full saturation (the colors should be visible and rich) and go around the circle, so from 0 to 360 degrees.

Let's get started then!

So first, we start with the usual boilerplating: A canvas and a script linking to the rainbow.

<!DOCTYPE html><html><head></head><body>  <canvas id="canvas" width="400" height="400"></canvas>  <script src="./rainbow.js"></script></body></html>

In there, I start with an array of arrays the same size as the canvas. I want to make this as generic as possible so I can also use it without the canvas or for any other gradient.

/** * Creates an array of arrays containing a gradient at a given angle. * @param valueFrom * @param valueTo * @param width * @param height * @param angle * @returns {any[][]} */const createGradientMatrix = (valueFrom, valueTo, width, height, angle) => {  let grid = Array(height)    .fill()    .map(      () => Array(width).fill(null)    )  // ...}

I also normalize valueTo, so I can use percentages to determine which value I want. For example, 50% is should be halfway between valueFrom and valueTo.

const normalizedValueTo = valueTo - valueFrom

Determining the color of a pixel

This is where the mathematics come in. In a gradient, all pixels lie on parallel lines. All pixels on the same line have the same colors. A line is defined as follows:

y=mx+ay = mx + ay=mx+a

Where m is the slope of the line and a describes the offset on the Y axis.

Demos can illustrate that pretty well:

Desmos showing a single line.

Now, to create a gradient, we can gradually increase the Y axis offset and start to color the lines differently:

Desmos showing a linear gradient.

Now, how can we use this to determine the color of each and every pixel?

We need to figure out which line it is on. The only difference between all the lines of the gradient shown with Desmos is the Y axis offset a. We know the coordinates X and Y of the pixel and we know the slope (given by the angle), so we can determine the Y axis offset like this:

a=ymxa = y - m * xa=ymx

We can define this as a JS function right away:

/** * Determines the a of `y = mx + a` * @param x * @param y * @param m * @returns {number} */const getYOffset = (x, y, m) => y - m * x

Now we know the line the pixel is on. Next, we need to figure out which color the actually has. Remember how we normalized the valueTo in order to figure out a value with percentages? We can dos something similar here:

const createGradientMatrix = (valueFrom, valueTo, width, height, angle) => {  // ...  // Some trigonometry to figure out the slope from an angle.  let m = 1 / Math.tan(angle * Math.PI / 180)  if (Math.abs(m) === Infinity) {    m = Number.MAX_SAFE_INTEGER  }  const minYOffset = getYOffset(width - 1, 0, m)  const maxYOffset = getYOffset(0, height - 1, m)  const normalizedMaxYOffset = maxYOffset - minYOffset  // ...}

By plugging in the maximum X value (width - 1) and the maximum Y value (height - 1) we can find the range of Y offsets that will occur in this gradient. Now, if we know the X and Y coordinates of a pixel, we can determine it's value like so:

const yOffset = getYOffset(x, y, m)const normalizedYOffset = maxYOffset - yOffsetconst percentageOfMaxYOffset = normalizedYOffset / normalizedMaxYOffsetgrid[y][x] = percentageOfMaxYOffset * normalizedValueTo

So, this is what's happening now, step by step:

  • Transform the angle of all lines into the slope of all lines
  • Do some failover (if (Math.abs(m) === Infinity) ...) to not run into divisions by zero etc.
  • Determine the maximum Y axis offset we'll encounter
  • Determine the minimum Y axis offset we'll encounter
  • Normalize the maximum Y axis offset, so we don't have to deal with negatives
  • Figure out the Y axis offset of the line that goes through X and Y
  • Normalize that calculated Y axis offset as well
  • Figure out how far (in %) this line is in the gradient
  • Use the calculated % to figure out the color value of the line
  • Assign the color value to the pixel

Let's do that for every pixel of the grid:

/** * Determines the a of `y = mx + a` * @param x * @param y * @param m * @returns {number} */const getYOffset = (x, y, m) => y - m * x/** * Creates an array of arrays containing a gradient at a given angle. * @param valueFrom * @param valueTo * @param width * @param height * @param angle * @returns {any[][]} */const createGradientMatrix = (valueFrom, valueTo, width, height, angle) => {  let grid = Array(height)    .fill()    .map(      () => Array(width).fill(null)    )  // Some trigonometry to figure out the slope from an angle.  let m = 1 / Math.tan(angle * Math.PI / 180)  if (Math.abs(m) === Infinity) {    m = Number.MAX_SAFE_INTEGER  }  const minYOffset = getYOffset(width - 1, 0, m)  const maxYOffset = getYOffset(0, height - 1, m)  const normalizedMaxYOffset = maxYOffset - minYOffset  const normalizedValueTo = valueTo - valueFrom  for (let x = 0; x < width; x++) {    for (let y = 0; y < height; y++) {      const yOffset = getYOffset(x, y, m)      const normalizedYOffset = maxYOffset - yOffset      const percentageOfMaxYOffset = normalizedYOffset / normalizedMaxYOffset      grid[y][x] = percentageOfMaxYOffset * normalizedValueTo    }  }  return grid}

This will yield an array of arrays the size of the canvas with values for each cell between valueFrom and valueTo.

Creating the actual rainbow

Let's use this to create a rainbow:

const canvas = document.querySelector('#canvas')const context = canvas.getContext('2d')const grid = createGradientMatrix(0, 360, 400, 400, 65)grid.forEach((row, y) => row.forEach((cellValue, x) => {  context.fillStyle = 'hsl('+cellValue+', 100%, 50%)'  context.fillRect(x, y, 1, 1)}))

You can now see that the gradient matrix we've created isn't necessarily for canvasses only. We could also use this to create colored text:

const loremIpsum = 'Lorem ipsum ...' // Really long text here.const lines = loremIpsum.substring(0, 400).match(/.{1,20}/g)const loremMatrix = lines.map(l => l.split(''))const textColorGrid = createGradientMatrix(0, 360, 20, 20, 65)for (let x = 0; x < 20; x++) {  for (let y = 0; y < 20; y++) {    loremMatrix[y][x] = `      <span class="letter" style="color: hsl(${textColorGrid[y][x]}, 100%, 50%);">        ${loremMatrix[y][x]}      </span>`  }}const coloredText = loremMatrix.map(l => l.join('')).join('')document.querySelector('#text').innerHTML = coloredText

The result

And here's the result:

Awesome! And it just started raining again...

I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a or a ! I write tech articles in my free time and like to drink coffee every once in a while.

If you want to support my efforts, buy me a coffee or follow me on Twitter ! You can also support me directly via Paypal!

Buy me a coffee button

(Cover image by Flickr user Ivan, released under CC by 2.0, no changes made to the image)


Original Link: https://dev.to/thormeier/let-s-build-a-rainbow-on-a-canvas-from-scratch-40l5

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To