Hand-coding a color wheel with canvas

Cory Forsyth
10 min readFeb 12, 2017

--

The other day I found myself staring my monitor as my Mac disgracefully spun the beachball of death at me, and in addition to wondering what I had done to deserve this fate I started to wonder how one might describe the algorithm that distributes those colors with code. When my Mac finally came back to life I decided to play around a little and see if I could do it. This post is the result of that exploration.

By the end of this post I’ll have shown you how to use canvas to make a color wheel on the web, similar to Apple’s circular color picker:

Our goal: Render a color wheel like Apple’s HSL color picker picture here

When faced with any new programming challenge, it’s critical to break it down into smaller steps that are more approachable—by building those and layering them atop each other it’s possible to achieve our more ambitious overall goal. In the case of this post the steps will be:

  • Figure out how to just draw something (in this post it will be a red square)
  • Refine our drawing so that we restrict the pixels to a circle
  • Find out hwo to map colors to specific angles around the circle
  • Voila: a color wheel!

I’ve identified the HTML5 <canvas> element as our first building block. We’ll eventually use it to draw the entire color wheel, but the first step is figuring out how to instruct it to draw basic color data, pixel by pixel. To do this we use the companion methods createImageData and putImageData. createImageData generates a Uint8ClampedArray that represents the pixels in the canvas. By manipulating the elements of that array, it’s possible to set pixel color values that can then be rendered usingputImageData.

The Hello World of images

Setting the color of a particular pixel is conceptually simple: simply put that color data into the image data array at the right spot. Figuring out the correct position is tricky, though. The image is two-dimensional, but the data array is one-dimensional (flat). It represents each pixel in the image going across the rows from left to right, starting in the upper left corner. Furthermore, each pixel is represented with integers in four spots in the array (for red, green, blue and alpha values). So the r,g,b,a values of the upper-leftmost pixel are represented by the first four indices in the array. If you have a 100-pixel-wide image, for example, the first 400 indices in the array represent the rgba values of the pixels across the top row. The rgba values of the leftmost pixel of the second row start at the 400th index (and continue through the 403rd).

Let’s make a 100x100 canvas and just shove a bunch of red pixels into it. The image data array will have a length of 100 by 100 by 4, and we’ll iterate through it, putting 255 into every 1st pixel (for “full” red) and 255 into every 4th pixel (for “full” alpha—completely opaque):

I won’t embed a codepen that only shows a red square (I trust you can envision it), but you can click through to check it out if you’re so inclined.

This is a good initial start that we can already play around with in fun ways. Let’s bind the r, g, b, and a values to sliders. Every pixel in our square is still the same color, but we can explore the RGB color space this way:

Drag the sliders around to change the color

Quick: Try to make yellow. Now try orange.

An aside on color theory

It may or may not be surprising that moving all sliders to the right (100% red, green and blue) yields white. This is because RGB is an additive color model, which behaves as though you are combining light of the various colors. And white light is what you get when you mix red, green and blue light. That’s why you see a rainbow bend out of a prism: those colors are already present in the “white” light.

Source: Filipe Saraiva

If you try mixing colors that are in a subtractive color model, such as paint, equal parts of red, green and blue will not combine to make white. Recall finger painting as a child—after swirling around all the colors into a great goopy mess you end up with a brownish/blackish hue. A common subtractive color palette is cyan, magenta, yellow and black (CMYK; the k is black and stands for “key”). Does it seem odd that RGB can apparently make all the colors using only 3 variables but CMYK seems to require 4? Well, C, M and Y are enough in theory to make all colors (including black), but messy reality gets in the way when printing, and the black that results from evenly mixing equal amounts of cyan, magenta and yellow is “unsatisfactory”—not a very deep black. In addition, black stuff (like text) is common when printing and it’s cheaper to use only a single ink (the black one) instead of 3. There are several other benefits of using black ink.

If you play around further with the codepen above you’ll see that the additive and subtractive color models are correlated: cyan, magenta and yellow are the result of combining each two-item combination of R,G and B.

We’ve now covered the basics of pixel manipulation in canvas, along with an iota of color theory. We’ll return to color soon, but for now we’re moving on to the “wheel” part of color wheel. Let’s draw a circle.

Draw a circle

A circle is defined by its radius. Everything that is within the radius’s distance from the circle’s center is in the circle, everything that is farther away is out of it. Pretty straightforward. Our next task, then, is to figure out how far away every pixel in our square is from the center, and skip all the ones that are outside the circle. This is the first time we’ve had to think about the location of the pixels, and to simplify our ability to do that, we’re going to change the way we loop through the pixels. Our code used to be one single loop from 0 to 40,000 (100 pixels by 100 pixels by 4 values per pixel), in groups of 4. When we draw a circle, it’s easiest if we use an x-y coordinate system where the origin is the center of the circle. When we do this it’s much simpler to figure out the distance from any (x,y) point to the center. We do it by employing a 2000-year-old formula: the pythagorean theorem.

If you have a point (x,y) and you imagine a line from it vertically to the x-axis (call this line a) and another one horizontally to the y-axis (call it b), a line to the center forms the hypotenuse (line c) of a right triangle with those two other lines. Solve the pythagorean theorem for c to get that distance.

a^2 + b^2 = c^2

In JavaScript, then:

let distance = Math.sqrt(x*x + y*y);

Updating the draw function to now be drawCircle, with a new x-y coordinate system centered at (0,0), we have:

Since our circle’s radius is 50 pixels, we loop the x and y values from -50 to 50 symmetrically around a central (0,0) origin. This simplifies the calculation of the distance of the (x,y) coordinate but complicates the part of the code where we figure out what index in the image data array corresponds to that pixel. This wasn’t a problem before when we were only looping from 0 to 40,000. To translate from the coordinates used in our loop to those in the data array, we first adjust each x and y value by adding the radius to translate them from the range [-50,50] to [0,100]. Then multiply the adjusted y value by the length of a single row of the canvas (100 pixels, in this case) and add the adjusted x value. Finally, because each pixel is represented by 4 values in the array, multiply by pixelWidth (4). This is the line of code that calculates the index in the pixel data array:

let index = (adjustedX + (adjustedY * rowLength)) * pixelWidth;

And here’s the same pen as before, with the new code from above:

To recap:

  • We can put a pixel of any RGB color of our choosing onto the canvas at any (x,y) coordinate that we desire.
  • We can restrict our output to a circle.

As a reminder, here’s the goal:

How can we map each point in the circle to a color such that there is a smooth transition of the hue as we sweep 360 degrees around? To do this effectively, we need a color system that is more of a natural fit for this visual representation. We’ll abandon RGB temporarily and take a look at a color system based on hue, saturation and value, aka HSV. This is a cylindrical-coordinate color system where hue maps directly to the degrees of a circle. This is perfect for making a color wheel!

HSV cylindrical representation. Source

RGB: The color cube

We certainly could attempt to map RGB colors such that they smoothly transition around our circle, but it would be quite hard. If you consider that RGB was designed to map to a cube, you can understand why that might make it less straightforward to map those colors to a circle instead.

RGB Color cube. Source

For a better visual understanding of why hue works so well for our purposes, we’ll start by redoing the RGB sliders example from above to use HSV sliders instead. Unfortunately, the data that we render to the canvas using putImageData still requires that we set r,g,b,a values, not h,s,v values. So we have to write a function that translates between the two. Here’s the code. If you want to understand it better I’ll direct you to the HSL and HSV article on wikipedia.

Try playing with this codepen that uses HSV sliders. If you drag the hue slowly from left to right you’ll see the square change smoothly from red through the rainbow and back to red. Notice that the rgb values change a bit more haphazardly.

Ok, we can map HSV color to RGB values so that we can display it on our canvas. How do we map each (x,y) point in the circle to a hue value? Or, to be more precise, how do you figure out the angle of each (x,y) point?

The answer: polar coordinates! Just as (r,g,b) uses cartesian coordinates and (h,s,v) ended up being more useful for our circular space because it uses cylindrical coordinates, polar coordinates are the best tool for exploring a 2-dimensional space circularly. Each pixel in an (x,y) system is located by its distance to the right (x-axis) and up (y-axis) from the origin, in a polar coordinate system each pixel is defined by its distance from the origin (r) and its angle (phi).

That said, the canvas pixel data array is still organized for (x,y) pixels, so when it comes time to render onto the canvas we’ll have to map those (x,y) coords into polar ones. We actually are half-way there already — the distance calculation we did for the circular codepen above gives the r value for each point. We are only missing the angle phi for each point. To do so, remember the triangle that the (x,y) point makes with the origin, and take the arctangent of the two orthogonal sides of it: phi = arctangent(dy, dx). JavaScript implements arctangent with the Math.atan2 function that returns results in radians in the range [-π,π]. We’re mapping the hue to degrees of the circle, though, so we have to convert radians to degrees.

Check it out!

Awesome. We are basically there, but you’ll notice that the colors come to a bit of a discontinuous pinch point in the center. This would look nicer if they all blended nicely there. We want the colors to be most vibrant at the edge and fade to white in the center. So far we’ve only varied the HSV of each (x,y) pixel by its angle (phi) — now we’ll make each pixel’s color a function of both the angle and its distance (r). The hue is still mapped to degree, and now the saturation will be mapped to distance, from 0.0 (in the center) to 1.0 at the edge of the circle.

Voila!

Bonus: Let’s spice this up by animating a new property that I’m calling swirl, which shifts the hue by an amount that increases the farther away the pixel is from the center. Building animations into the color wheel will be the topic of a future post.

--

--

Cory Forsyth

Lifelong learner and technologist, based in NYC. Engineer @ Addepar, formerly co-ran Ember consultancy 201 Created.