Up: GROVE
Last edit: <2021-01-11>

reasoning about colors


In July 2020 I went on a color-scheme vision quest. This led to some research on various color spaces and their utility, some investigation into the styling guidelines outlined by the base16 project, and the color utilities that ship within the GNU Emacs text editor. This article will be a whirlwind tour of things you can do to individual colors, and at the end how I put these blocks together.

1 Motivation

I've been a part of several Linux desktop customization communities since circa 2013. One big aspect of that is the colors used across various contexts – for me, it follows that part of the game is trying to make a cohesive system of colors that relate to each other in an understandable (and thus tweakable) way – know what I can do to individual colors when making a "color framework" helps immensely.

I'm colorblind. This means I might be really picky about some colors. For example, I don't like the color red used for emphasis in text – thin red lines look the same as thin black lines to me (and so, red text doesn't typically >POP< for me, unless it's bold or has some other emphasis included).

Bootstrapping builders exist for base16! If I can bootstrap on top of their system I get a lot of free coverage within the software ecosystem.

Plus, I just find this sort of thing really fun. Visual feedback is pleasing. Finding the right colors makes my lizard brain return to monke.

2 Side note: The Canvas

This will be the focal point of inconsistency. The level of brightness, quality of screen, and ambient lighting level are all things that affect the value of the screen's white point, which is what everything else is relative too. Luckily you can (attempt to) account for this as well.

3 Color Spaces

Color spaces are ways of defining colors in different sets of properties. They are the main tool you will have for reasoning about tweaking individual colors. You can then mess with these and convert them back into a format you can render (typically RGB) within a color gamut (a range of supported colors). Here I will be pretty high level, focusing on some visuals for what sorts of things these properties look like. When I define the valid values for ranges, I will be using the scale I've implemented in my helpers.

3.1 RGB

The one you know and love: [R]ed, [G]reen, [B]lue. Your knobs are amounts of each. As you turn everything up, you approach #ffffff (and down, -> #000000). This isn't particularly flexible in "ways you can think about colors".

Here is a gradient from #cc3333 to #33cc33 to #3333cc:

To show the lighting effect, let's repeat the above gradient, but instead of using 33 for filler, we'll use 99 – that's triple(!) the secondary color amounts:

3.2 HSL

wikipedia: HSL and HSV

[H]ue 0-360 Color "direction"
[S]aturation 0-100 Color "strength"
[L]ightless 0-100 Light level

Saturation in HSL is a controlled version of chromacity ("distance from gray"). See the wiki section for more details.

color_cylinder.png

Hue has several defined points (at rotating 60° angles), I like to think of it like a color compass:

red, 0°
yellow, 60°
green, 120°
cyan, 180°
blue, 240°
magenta, 300°

HSL: Hue rotation 0-360 (step 60°), saturation 50%, lightness 50%

Let's see the effect saturation has:

HSL: saturation scale 0-100% (step 10%), lightness 50%, hue 240° (blue)

And lightness:

HSL: lightness scale 0-100% (step 10%), saturation 50%, hue 240° (blue)

3.3 HSLuv

hsluv is an altered version of HSL that tries to be perceptually uniform with regards to lightness. HSL lightness by comparison is hard to make contrast comparisons in.

What does that mean for us? Well, let's take our above examples and recreate them in the HSLuv space:

red, 0°
yellow, 60°
green, 120°
cyan, 180°
blue, 240°
magenta, 300°

HSLuv: Hue rotation 0-360 (step 60°), saturation 50%, lightness 50%

Saturation:

HSLuv: saturation scale 0-100% (step 10%), lightness 50%, hue 240° (blue)

Lightness:

HSLuv: lightness scale 0-100% (step 10%), saturation 50%, hue 240° (blue)

These scales definitely look more consistent when reasoning about lightness values. HSL's hue feels all over the place by comparison – though at the same time that might be a more natural color mixing feel.

3.4 CIELAB

wikipedia link

[L]ightness 0-100 Light level
[A] toggle -100-100 green <–> red
[B] toggle -100-100 blue <–> yellow
whitepoint coordinates [X, Y, Z] a point in the CIE XYZ space that defines "white" from the perspective of the image being displayed

The white point is a defined standard illuminate not intrinsic to the value of a color. It is an additional piece of information you provide to functions when converting into and out of the CIELAB colorspace.

The standard white point is defined as d65 – in this section, every conversion will be made with d65. Here is a table of commonly used white points and their meaning (for values, see the bottom of the wikipedia link).

d65 Noon Daylight: Television, sRGB color space (standard assumption)
d50 Horizon Light. ICC profile PCS
d55 Mid-morning / Mid-afternoon Daylight
d75 North sky Daylight

The knobs A and B allow you to play with the 4 primary colors of the LAB space. If you take a look at the values, you might notice that the more negative we go, we get "cooler" colors, while on the positive end, we get "warmer" colors.

Let's look at some LAB colors. The labels below will have the values of (L A B) – Remember, A is green to red, B is blue to yellow (each with a value -100 to 100)

(50,-80,0)
(50,-60,0)
(50,-40,0)
(50,-20,0)
(50,0,0)
(50,0,0)
(50,20,0)
(50,40,0)
(50,60,0)
(50,80,0)

(50,0,-80)
(50,0,-60)
(50,0,-40)
(50,0,-20)
(50,0,0)
(50,0,0)
(50,0,20)
(50,0,40)
(50,0,60)
(50,0,80)

(50,-80,-80)
(50,-60,-60)
(50,-40,-40)
(50,-20,-20)
(50,0,0)
(50,0,0)
(50,20,20)
(50,40,40)
(50,60,60)
(50,80,80)

lab scales: -A -> +A, -B -> +B, {-A,-B} -> {+A,+B}

3.5 LCH

[L]uminance 0-100 Light level
[C]hromacity 0-100 Distance from gray
[H]ue 0-360 Color "direction"

LCH is a "cylindrical" version of cieLAB. What that means for us is that Hue is different. Instead of 6 defined islands to sail to with our color compass, there are 4:

red, 0°
yellow, 90°
green, 180°
blue, 270°

LCH: Hue rotation 0-360 (step 90°), saturation 50%, luminance 50%

LCH lightness:

LCH: lightness scale 0-100% (step 10%), chromacity 50%, hue 270° (blue)

Chromacity, "distance from gray" - very similar to Saturation (which I've seen cited as simply misnamed chromacity):

LCH: chromacity scale 0-100% (step 10%), luminance 70%, hue 270° (blue)

3.6 Property comparison

Let's compare some spaces. We'll take some the RGB gradient from above, normalize the lightness in HSLuv and then maximize l[C]h, H[S]L, and H[S]Luv:

original

squash lightness to 50 in HSLuv

3 branches off of the above: LCH maximize C, HSL maximize S, HSLuv maximize S

4 Other stuff

4.1 Contrast

For text, the Web Content Assembly Guidelines (WCAG) recommend at least a 4.5:1 contrast ratio: link. Let's take a look at some different text contrasts! I will steal the backgrounds used here from the base-16 grayscale sets: #f7f7f7 and #101010. For reference, the contrast ratio between #000000 and #ffffff is 21.0

Dark:

3.0: Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
4.0: Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
5.0: Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
6.0: Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
7.0: Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
8.0: Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.

dark contrast ratios, 3.0 - 9.0, step 1.0

Light:

3.0: Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
4.0: Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
5.0: Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
6.0: Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
7.0: Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
8.0: Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.

light contrast ratios, 3.0 - 9.0, step 1.0

I think it's pretty clear from these examples that higher contrast goes a long way in dark color schemes.

4.2 Distance

Color distance is a measure of how far apart colors are by properties in spaces. For example, let's take the 'magenta' color from above, and increase it's brightness and hue until we're some minimal distance away. We'll aim for 33(out of 100) measured in the CIELAB space:

0
3
7
10
14
18
22
27
31
35

CIELAB distance from the start color is shown

Color distance is useful because it lets us measure a kind of similarity between colors. You can use this to control where you stop transformations (color space property tweaks).

4.3 Gradients

A gradient is where you travel from one color's initial property values to some other color's property values, collecting the intermediate steps.

4.4 Pastel

"Pastel Colors" when described in HSL have high lightness and low saturation. This means we can invent a function to "pastelize" a color, bit by bit (increasing lightness and lowering saturation). Let's take a rather dark defined color #2d249f, and run it through with the same effect we have at the top of this page, making it more pastel until it's pretty bright:

4.5 Colorwheel rotations

Color wheel rotations are all about hue. The circle that hue forms is the color wheel for that color space. Colors that are opposed here (180° away from each other) are complementary colors. One way to attempt to generate color palettes is to do "color wheel rotations" where you take colors around equidistant points around the color wheel. The hue values we've been showing are examples of a color wheel rotation (6 points around 60°)

Let's say we we've played around in the LAB space to find a warm looking light background LAB(90,90,10), and then we darken it until we hit some minimal contrast (say, 3.9) for a starting color #816557, which has a hue of 19.6°. Let's see what doing hue rotations on this color look like:

19.6
139.6
259.6

HSL: 120° rotation (hue value shown)

19.6
109.6
199.6
289.6

HSL: 90° rotation (hue value shown)

19.6
91.6
163.6
235.6
307.6

HSL: 72° rotation (hue value shown)

19.6
79.6
139.6
199.6
259.6
319.6

HSL: 60° rotation (hue value shown)

19.6
64.6
109.6
154.6
199.6
244.6

HSL: 45° rotation (take 6) (hue value shown)

Rotations around hue in different color spaces will yield different results. This can be a way to derive accent colors for use in a color-scheme.

4.6 white-point adjustment

CIELAB has a white point component used when entering and leaving the space. You can adjust the white point value that you use going into and then coming out of the space, allowing you to "adjust colors by white point". This is kind of a weird concept. Let's take the gradient at the top of this page and pass it into LAB with d65 (the standard assumption, sRGB) but pull it out using d50 ("Horizon Light, ICC profile PCS"). (This effect is mostly visible on grayscale colors, and esp on the lighter end):

original

transformed

Mapping color palettes through this transform could presumably get you better results in different lighting conditions. I've not played with it too much.

5 Implementing helpers

This section is about the tools I implemented and use to actually do the thing™.

Emacs ships with a fair amount of conversion functions, but using them to convert between color spaces can be awkward. You end up with a lot of pipelines to glue color-name-to-rgb, color-srgb-to-lab, color-lab-to-lch, and pipe back out. To assist with this, I implemented some wrappers that would do the conversion to your space of choice (coming from the 'name', strings eg "#c930e8"). Here's an example – say you wanted to increase luminosity of that color by a multiplier 1.5:

(ct-transform-hsl "#c930e8"
 (lambda (H S L)
   (list H S (* 1.5 L))))

;; => "#eb16af59f708"

#eb16af59f708 is definitely a lighter color, nice.

Side note for the notation here: Emacs colors use 4 bytes, not 2, which is why we have such a long boy there. When I export to HTML I use do a pass to shorten the color into a 2 byte space so the browser can render it.

I also implemented a function for comparing contrast, referencing Peter Occil's wonderful color notes:

;; order does not matter:
(ct-contrast-ratio "#ffffff" "#445544")

;; => 3.0000000000000004

Is a color light? just check the lightness value in LAB space (note: that 65 value is ~opinions~):

(defun ct-is-light-p (name)
  (> (first (ct-name-to-lab name))
     65))

A neat trick you can do with this is decide whether or not to use a dark or light foreground against the color:

dark
light
dark
light
light

These pieces (transformers and comparison functions) can be combined to do things like "darken this color until I reach a minimum contrast ratio" (which is how I get theme-level contrast tweaking of foreground and accent colors). Enter ct-iterate – a function that takes an initial color, and applies a function to it until a condition is met (or if the transformation does nothing – you can't darken #000000!)

(ct-iterate
 "#eeeeee"

 ;; Darken the color a little at a time in LAB space:
 (lambda (c)
   (ct-transform
   -lab c
    (lambda (L A B)
      (list (- L 0.1)
            A B))))

 ;; check that we've reached some desired contrast ratio
 ;; 4.5, Here against a background #f7f7f7
 (lambda (c)
   (> (ct-contrast-ratio "#f7f7f7" c)
      4.5)))

;; => "#2d662ca72d1b"
;; (converted: #2d2c2c)

6 Vision quest

Alright, we've gone through a fair amount of ways you can play with individual colors. How could we use this?? What I ended up doing was coming up with a list of color types that I wanted to use in different situations. After some tinkering and considering I arrived at this list:

label meaning example
:foreground default foreground  
:foreground_ faded foreground comments
:foreground+ emphasized foreground urgent notification
:background default background  
:background_ faded background modeline
:background__ alternate background code block background
:background+ emphasized background highlighted text
:accent1 (foreground) identity functions, variables
:accent1_ (foreground) assumptions (faded accent1) builtins
:accent2 (foreground) accent2 types
:accent2_ (foreground) strings strings

The pair of accent2 colors turned out to be the most awkward here. I personally believe strings are important enough to get a standalone color, which is what accent2_ turned into. The accent1 idea of "lesser and greater" pairings cover a lot of ground, meaning that accent2 turned into a rarely used color – as I'm writing this I'm realizing maybe I could use accent2 to color scalar types in general (or expand the accent2_ definition to all scalar types).

[2021-01-03 09:45 AM] You can now see where I'm tracking these ideas in my tarps repo.

6.1 Methods

Now that I'd derived types of things I wanted, it was time to try out the techniques above to create colors fitting the slots:

  • color wheel rotations
  • complementary colors
  • contrast levels through iteration
  • "pastelize" until a minimum distance is reached
  • using L[C]H for emphasis

At the time of this writing, I'm using a color rotation of 45° in the LCH space (starting from my foreground_, which is a darkened version of background) focusing on the bluish side of things for the accent colors. I get my background+ by graying out my accent2 (lowering C in LCH), and then lightening it until there's a very low contrast between it and my background. For posterity, I will share this theme here:

:background
:background_
:background__
:background+
:foreground
:foreground_
:foreground+
:accent1
:accent1_
:accent2
:accent2_

Which in action looks like (click to see full size):

colors.png

I store these in a hash table in emacs, so that I can always query the current theme from anywhere (eg elisp -r '(ht-get ns/theme :accent1)), allowing me to use my intended color preferences across many contexts.

6.2 Bootstrapping

I like free things. There are many base16 builders, including one for emacs – if I can map my palette to it, I can get free support for a wide array of emacs plugins and builtin packages!

Much playing around with the base16 emacs theme builder led me to this mapping:

base16 label system label base16 standard meaning
:base00 :background Default Background
:base01 :background+ Lighter Background (Used for status bars)
:base02 :background+ Selection Background
:base03 :foreground_ Comments, Invisibles, Line Highlighting
:base04 :foreground_ Dark Foreground (Used for status bars)
:base05 :foreground Default Foreground, Caret, Delimiters, Operators
:base06 :foreground_ Light Foreground (Not often used)
:base07 :foreground_ Light Background (Not often used)
:base08 :accent2 Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted
:base09 :foreground Integers, Boolean, Constants, XML Attributes, Markup Link Url
:base0A :accent2 Classes, Markup Bold, Search Text Background
:base0B :accent2_ Strings, Inherited Class, Markup Code, Diff Inserted
:base0C :accent1_ Support, Regular Expressions, Escape Characters, Markup Quotes
:base0D :accent1 Functions, Methods, Attribute IDs, Headings
:base0E :accent1_ Keywords, Storage, Selector, Markup Italic, Diff Changed
:base0F :foreground_ Deprecated, Opening/Closing Embedded Language Tags, e.g. <?php ?>

This might look fairly comprehensive, but there's SO much room for ambiguity in editor specific situations – base16 builders are forced to make stylistic decisions that you might not agree with. At least with the emacs base16 builder I found myself making some tweaks after the fact.

Now that the mapping has been created, with some glue I can use any of the base16 builders, giving me access to a wide array of templates and outputs for use with my color palette! Having room to "echo" your color decisions across many different applications is very satisfying.

H
a
p
p
y
C
o
l
o
r
i
n
g
!

6.4 Thanks

Chris Kempson for the base16 project

Shoutout to belak for work on the the base16 emacs theme builder

Thanks to the Axis of Eval, camille, and xero for all their feedback when I was posting way too many pictures of colors.