Last week, we’ve had a look at how to distribute maximally different colors on the RGB cube. But I also remarked that we could use some other color space, say HSV. How do we distribute colors uniformly in HSV space?

Dispersing points on a square grid—the most intuitive choice—doesn’t distribute them equidistantly. Immediate neighbors are at distance 1, but the other, diagonal neighbors, are at a distance of √2. However, if we use a hexagonal lattice:

Each point is at a distance 1 of all its immediate neighbors. The grid is also not complicated to create: its basis is the equilateral triangle. Its basis vectors are

,

.

The combinations , with varying and will cover the entire lattice.

However, we’re interested only to the points of the lattice that will correspond to colors; say RGB-representable colors. The colorspace might be HSV, but only colors that are mappable to RGB (and our device’s gamut). Remember HSV: the hue is given by the angle made between a point and some reference axis. Each point of the lattice can have its angle measured against some reference vector, say (1,0), that would be red. Distance to origin is saturation and height is brightness. Let’s conveniently say that in 2D, brightness is distance to the origin (up to a radius of 1) and that saturation is always maximal.

What varies now is the scale of the grid: if we vary and coarsely, we have few colors, if we vary them finely, we have more.

*

* *

The number of colors generated by this scheme cannot be arbitrary: colors are placed on the lattice within a radius 1 of the origin, and varying the density of (and in the same way) will allow a limited number of colors to be generated: 1, 7, 19, 37, 61, 91, 127, …

These are centered hexagonal numbers, and the general formula to generate the sequence is

.

*

* *

To create the animation, I used the following Mathematica Code:

(* To make it simpler with vectors *) ATan[z_] := If[z == {0, 0}, 0, ArcTan[z[[1]], z[[2]]] ] Palette[d_] := Graphics[ { {White, Circle[{0, 0}, 1 + 1/(2 d)]}, { PointSize[1/(3 d + 1/2)], Table[ If[Norm[u x + y v] <= 1, { Hue[ATan[u x + y v]/(2 \[Pi]), 1, Sqrt[ Norm[u x + y v]]], Point[x u + y v] }] , {x, -2, 2, 1/d}, {y, -2, 2, 1/d}] } }, ImageSize -> 500 ]

One thing you may notice is that I do not use the norm, but its square root. Why? This gives better contrast—it’s a heuristic observation.

*

* *

If we use two dimensions for hue and saturation, we’re left with a third for value, or brightness:

The code is pretty much the same, except that we now have another dimension. The lattice is extended to 3D in a way that ensures the points remain equidistant not only in the hue/saturation plane but also between planes. That’s more or less the “cannonball stack” arrangement:

We now need three basis vectors:

,

,

.

and cycling through inside a cylinder (unit radius and height 2, centered at 0), with varying density produces the animation above. The Mathematica code is:

ATan2[u_] := ATan[u[[1 ;; 2]]] Palette3D[d_] := Graphics3D[ { { PointSize[1/(3 d + 2)], Table[ If[(Norm[(x u3 + y v3 + z w3) {1, 1, 0}] <= 1) && (Norm[z w3] <= 1), { Hue[ATan2[x u3 + y v3 + z w3]/(2 \[Pi]), Norm[(x u3 + y v3 + z w3) {1, 1, 0}], ((x u3 + y v3 + z w3)[[3]] + 1) 1/2 1/Sqrt[2/3] ], Point[x u3 + y v3 + z w3] } ] , {x, -2, 2, 1/d}, {y, -2, 2, 1/d}, {z, -1, 1, 1/d}] } }, Lighting -> None, RotationAction -> "Clip", SphericalRegion -> True, Boxed -> False, ImageSize -> 500 ]

*

* *

Either of the above methods are truly useful when the number of colors remains modest. We’re not very good at distinguishing colors. Maybe only a few tens of colors displayed simultaneously will appear different enough to be useful—we’re much, much better at differentiating brightness. Combining brightness and color maybe a bit better than using only color or only brightness.