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 – knowing 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 #
[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.
Hue has several defined points (at rotating 60° angles), I like to think of it like a color compass:
Let’s see the effect saturation has:
And lightness:
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:
Saturation:
Lightness:
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 #
[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)
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:
LCH lightness:
Chromacity, “distance from gray” - very similar to Saturation (which I’ve seen cited as simply misnamed chromacity):
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:
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:
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
Light:
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
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:
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 brighter:
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:
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):
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™.
Update ct.el
I have packaged my helpers into an emacs package:
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-edit-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:
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-edit-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).
tarps repo.
You can now see where I’m tracking these ideas in my6.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:
Which in action looks like (click to see full size):
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.
6.3. References and further reading #
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.