#+title: reasoning about colors #+pubdate: <2020-11-24> #+rss_title: reasoning about colors #+html_head: # TODO # harmonizing colors meaning # https://www.incolororder.com/2011/11/art-of-choosing-harmonizing-color.html #+BEGIN_SRC elisp :results raw :exports results (let* ((word "AESTHETICS") (colors (ct-gradient (length word) (tarp/get :foreground) (tarp/get :background) t))) (ns/blog-make-color-strip colors (-map 'string word))) #+end_SRC In July 2020 I went on a color-scheme vision quest. This led to some research on various [[https://en.wikipedia.org/wiki/Color_space][color spaces]] and their utility, some investigation into the [[http://chriskempson.com/projects/base16/#styling-guidelines][styling guidelines]] outlined by the base16 project, and the [[https://github.com/emacs-mirror/emacs/blob/master/lisp/color.el][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. ** [[#h-3fe0b0c6-76a6-4e9e-a061-66bd3ba54620][Motivation]] :PROPERTIES: :CUSTOM_ID: h-3fe0b0c6-76a6-4e9e-a061-66bd3ba54620 :END: 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. ** [[#h-3820d027-5602-4691-b9ca-b36aadd3871a][Side note: The Canvas]] :PROPERTIES: :CUSTOM_ID: h-3820d027-5602-4691-b9ca-b36aadd3871a :END: 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 [[https://en.wikipedia.org/wiki/White_point][white point]], which is what everything else is relative too. Luckily you can (attempt to) account for this as well. ** [[#h-a71813d2-7e36-4f52-b22c-87e22d4a2620][Color Spaces]] :PROPERTIES: :CUSTOM_ID: h-a71813d2-7e36-4f52-b22c-87e22d4a2620 :END: 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 [[https://en.wikipedia.org/wiki/Gamut][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 [[#h-cb3c6479-7d62-4028-8942-2b033bb1247a][helpers]]. *** [[#h-99356355-d54c-41d8-bc1a-6e14e29f42c8][RGB]] :PROPERTIES: :CUSTOM_ID: h-99356355-d54c-41d8-bc1a-6e14e29f42c8 :END: 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 {{{color(#ffffff)}}} (and down, -> {{{color(#000000)}}}). This isn't particularly flexible in "ways you can think about colors". Here is a gradient from {{{color(#cc3333)}}} to {{{color(#33cc33)}}} to {{{color(#3333cc)}}}: #+begin_src elisp :results raw :exports results (ns/blog-make-color-strip (append (ct-gradient 15 "#cc3333" "#33cc33" t) (cdr (ct-gradient 15 "#33cc33" "#3333cc" t)))) #+end_src 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: #+begin_src elisp :results raw :exports results (ns/blog-make-color-strip (append (ct-gradient 15 "#cc9999" "#99cc99" t) (cdr (ct-gradient 15 "#99cc99" "#9999cc" t)))) #+end_src *** [[#h-43869bc7-a7d1-410f-9341-521974751dac][HSL]] :PROPERTIES: :CUSTOM_ID: h-43869bc7-a7d1-410f-9341-521974751dac :END: [[https://en.wikipedia.org/wiki/HSL_and_HSV][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. {{{image(color_cylinder.png)}}} Hue has several defined points (at rotating 60° angles), I like to think of it like a color compass: #+begin_src elisp :results raw :exports results (ns/blog-make-color-strip (-reductions-from (lambda (acc new) ;; list (ct-edit-hsl acc (lambda (H S L) (list (+ 60 H) 50 50)))) ;; starting with 1% saturation (0% removes our hue entirely) (ct-make-hsl 0 50 50) (range 5)) '("red,0°" "yellow,60°" "green,120°" "cyan,180°" "blue,240°" "magenta,300°")) #+end_src {{{detail(HSL: Hue rotation 0-360 (step 60°), saturation 50%, lightness 50%)}}} Let's see the effect saturation has: #+begin_src elisp :results raw :exports results (ns/blog-make-color-strip (--map (ct-make-hsl 240 (* 10 it) 50) (range 11))) #+end_src {{{detail(HSL: saturation scale 0-100% (step 10%), lightness 50%, hue 240° (blue))}}} And lightness: #+begin_src elisp :results raw :exports results (ns/blog-make-color-strip (--map (ct-make-hsl 240 50 (* 10 it)) (range 11))) #+end_src {{{detail(HSL: lightness scale 0-100% (step 10%), saturation 50%, hue 240° (blue))}}} *** [[#h-c147b84d-d95b-4d2d-8426-2f96529a8428][HSLuv]] :PROPERTIES: :CUSTOM_ID: h-c147b84d-d95b-4d2d-8426-2f96529a8428 :END: [[https://www.hsluv.org/comparison/][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: #+begin_src elisp :results raw :exports results (ns/blog-make-color-strip (-reductions-from (lambda (acc new) ;; list (ct-edit-hsluv acc (lambda (H S L) (list (+ 60 H) 50 50)))) (ct-make-hsluv 0 50 50) (range 5)) '("red,0°" "yellow,60°" "green,120°" "cyan,180°" "blue,240°" "magenta,300°")) #+end_src {{{detail(HSLuv: Hue rotation 0-360 (step 60°), saturation 50%, lightness 50%)}}} Saturation: #+begin_src elisp :results raw :exports results (ns/blog-make-color-strip (--map (ct-make-hsluv 240 (* 10 it) 50) (range 11))) #+end_src {{{detail(HSLuv: saturation scale 0-100% (step 10%), lightness 50%, hue 240° (blue))}}} Lightness: #+begin_src elisp :results raw :exports results (ns/blog-make-color-strip (--map (ct-make-hsluv 240 50 (* 10 it)) (range 11))) #+end_src {{{detail(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. *** [[#h-9d5a1a9a-75d3-48f5-bf00-85332d9b023e][CIELAB]] :PROPERTIES: :CUSTOM_ID: h-9d5a1a9a-75d3-48f5-bf00-85332d9b023e :END: [[https://en.wikipedia.org/wiki/CIELAB_color_space][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 [[https://en.wikipedia.org/wiki/CIE_1931_color_space][CIE XYZ]] space that defines "white" from the perspective of the image being displayed | The white point is a defined [[https://en.wikipedia.org/wiki/Standard_illuminant][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) #+begin_src elisp :results raw :exports results (s-join "\n" (-map (lambda (colors) (apply 'ns/blog-make-color-strip (-unzip (-map (lambda (props) (list (apply 'ct-make-lab props) (format "(%s,%s,%s)" (nth 0 props) (nth 1 props) (nth 2 props)))) colors)))) '( ((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))))) #+end_src #+begin_src elisp :results raw :exports results (s-join "\n" (-map (lambda (colors) (apply 'ns/blog-make-color-strip (-unzip (-map (lambda (props) (list (apply 'ct-make-lab props) (format "(%s,%s,%s)" (nth 0 props) (nth 1 props) (nth 2 props)))) colors)))) '(((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))))) #+end_src #+begin_src elisp :results raw :exports results (s-join "\n" (-map (lambda (colors) (apply 'ns/blog-make-color-strip (-unzip (-map (lambda (props) (list (apply 'ct-make-lab props) (format "(%s,%s,%s)" (nth 0 props) (nth 1 props) (nth 2 props)))) colors)))) '(((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))))) #+end_src {{{detail(lab scales: -A -> +A, -B -> +B, {-A,-B} -> {+A,+B})}}} *** [[#h-c4f93e1f-4fa6-4ebc-99c1-18b6de0ef413][LCH]] :PROPERTIES: :CUSTOM_ID: h-c4f93e1f-4fa6-4ebc-99c1-18b6de0ef413 :END: | [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: #+begin_src elisp :results raw :exports results (ns/blog-make-color-strip (-reductions-from (lambda (acc new) ;; list (ct-edit-lch acc (lambda (L C H) (list L C (+ 90 H))))) (ct-make-lch 50 50 0) (range 3)) '("red, 0°" "yellow, 90°" "green, 180°" "blue, 270°" )) #+end_src {{{detail(LCH: Hue rotation 0-360 (step 90°), saturation 50%, luminance 50%)}}} LCH lightness: #+begin_src elisp :results raw :exports results (ns/blog-make-color-strip (--map (ct-make-lch (* 10 it) 50 270) (range 11))) #+end_src {{{detail(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): #+begin_src elisp :results raw :exports results (ns/blog-make-color-strip (-reductions-from (lambda (acc new) ;; list (ct-edit-lch acc (lambda (L C H) (list L ;; correct for our starting position (+ 10 (* 10 (first (cl-round C 10)))) H)))) (ct-make-lch 50 0 270) (range 10))) #+end_src {{{detail(LCH: chromacity scale 0-100% (step 10%), luminance 70%, hue 270° (blue))}}} *** [[#h-836f1aa2-fddd-4a5f-b192-6675e463a1d9][Property comparison]] :PROPERTIES: :CUSTOM_ID: h-836f1aa2-fddd-4a5f-b192-6675e463a1d9 :END: 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: #+begin_src elisp :results raw :exports results (ns/blog-make-color-strip (append (ct-gradient 15 "#cc3333" "#33cc33" t) (cdr (ct-gradient 15 "#33cc33" "#3333cc" t)))) #+end_src {{{detail(original)}}} #+begin_src elisp :results raw :exports results (ns/blog-make-color-strip (-map (lambda (c) (-> c (ct-edit-hsluv (lambda (H S L) (list H S 50))))) (append (ct-gradient 15 "#cc3333" "#33cc33" t) (cdr (ct-gradient 15 "#33cc33" "#3333cc" t))))) #+end_src {{{detail(squash lightness to 50 in HSLuv)}}} #+begin_src elisp :results raw :exports results (s-join "\n" (list (ns/blog-make-color-strip (-map (lambda (c) (-> c (ct-edit-hsluv (lambda (H S L) (list H S 50)))) (-> c (ct-edit-lch (lambda (L C H) (list L 100 H)))) ) (append (ct-gradient 15 "#cc3333" "#33cc33" t) (cdr (ct-gradient 15 "#33cc33" "#3333cc" t))))) (ns/blog-make-color-strip (-map (lambda (c) (-> c (ct-edit-hsluv (lambda (H S L) (list H S 50)))) (-> c (ct-edit-hsl (lambda (H S L) (list H 100 L)))) ) (append (ct-gradient 15 "#cc3333" "#33cc33" t) (cdr (ct-gradient 15 "#33cc33" "#3333cc" t))))) (ns/blog-make-color-strip (-map (lambda (c) (-> c (ct-edit-hsluv (lambda (H S L) (list H S 50)))) (-> c (ct-edit-hsluv (lambda (H S L) (list H 100 L)))) ) (append (ct-gradient 15 "#cc3333" "#33cc33" t) (cdr (ct-gradient 15 "#33cc33" "#3333cc" t))))))) #+end_src {{{detail(3 branches off of the above: LCH maximize C, HSL maximize S, HSLuv maximize S)}}} ** [[#h-e1c795a7-b3d9-4be3-9874-1b98a2069520][Other stuff]] :PROPERTIES: :CUSTOM_ID: h-e1c795a7-b3d9-4be3-9874-1b98a2069520 :END: *** [[#h-c9cde0e6-ddb0-4f76-82ff-d730a3ce3f51][Contrast]] :PROPERTIES: :CUSTOM_ID: h-c9cde0e6-ddb0-4f76-82ff-d730a3ce3f51 :END: For text, the Web Content Assembly Guidelines (WCAG) recommend at least a 4.5:1 contrast ratio: [[https://www.w3.org/TR/WCAG/#contrast-minimum][link]]. Let's take a look at some different text contrasts! I will steal the backgrounds used here from the base-16 grayscale sets: {{{color(#f7f7f7)}}} and {{{color(#101010)}}}. For reference, the contrast ratio between {{{color(#000000)}}} and {{{color(#ffffff)}}} is 21.0 Dark: #+begin_src elisp :results raw :exports results (s-join "\n" `( "@@html:
@@" ,@(-map (lambda (fg) (ns/blog-make-color-block (/ 100.0 3.0) "#101010" ;; (second fg) (format "%s: %s" (second fg) "Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.") ;; (second fg) (first fg) "colorblock colorpadding" )) (cdr (-reductions-from (lambda (acc new) (list (ct-tint-ratio (first acc) "#101010" new ) new)) '("#101010") (-map 'float (range 3 9))))) "@@html:
@@" )) #+end_src {{{detail(dark contrast ratios, 3.0 - 9.0, step 1.0)}}} Light: #+begin_src elisp :results raw :exports results (s-join "\n" `( "@@html:
@@" ,@(-map (lambda (fg) (ns/blog-make-color-block (/ 100.0 3.0) "#f7f7f7" (format "%s: %s" (second fg) "Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut
labore et dolore magna aliqua.") (first fg) "colorblock colorpadding" )) (cdr (-reductions-from (lambda (acc new) (list (ct-tint-ratio (first acc) "#f7f7f7" new) new)) '("#f7f7f7") (-map 'float (range 3 9))))) "@@html:
@@" )) #+end_src {{{detail(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. *** [[#h-e260bdea-3408-47e6-a195-f5a62ed979bc][Distance]] :PROPERTIES: :CUSTOM_ID: h-e260bdea-3408-47e6-a195-f5a62ed979bc :END: 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: #+begin_src elisp :results raw :exports results (apply 'ns/blog-make-color-strip (-unzip (-map (lambda (c) (list c (floor (ct-name-distance (ct-make-hsluv 300 50 50) c)))) (ct-iterations (ct-make-hsluv 300 50 50) ;; "#a0d55df5acf4" (lambda (c) (ct-edit-hsluv c (lambda (H S L) (list (+ H 10) S (* L 1.05))))) (lambda (c) (> (ct-name-distance (ct-make-hsluv 300 50 50) c) 33.0 )))))) #+end_src {{{detail(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). *** [[#h-91fbcdc5-10ac-40ab-93d8-0d64cb1c7d01][Gradients]] :PROPERTIES: :CUSTOM_ID: h-91fbcdc5-10ac-40ab-93d8-0d64cb1c7d01 :END: A gradient is where you travel from one color's initial property values to some other color's property values, collecting the intermediate steps. *** [[#h-1ed7ea90-395e-4486-a11c-6f3c9054dd15][Pastel]] :PROPERTIES: :CUSTOM_ID: h-1ed7ea90-395e-4486-a11c-6f3c9054dd15 :END: "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 {{{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: #+begin_src elisp :results raw :exports results (let* ((word "AESTHETICS") (colors (-map (fn (-reduce-from (lambda (acc new) (ct-pastel acc)) "#2d249f" (range (+ 1 <>)))) (range (length word))))) (ns/blog-make-color-strip colors (-map 'string word))) #+end_src *** [[#h-81b4122f-f725-45ec-8c4a-437688cbcc2a][Colorwheel rotations]] :PROPERTIES: :CUSTOM_ID: h-81b4122f-f725-45ec-8c4a-437688cbcc2a :END: 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 {{{color(#ffffd53ed101, LAB(90\,90\,10))}}}, and then we darken it until we hit some minimal contrast (say, 3.9) for a starting color {{{color(#816557)}}}, which has a hue of 19.6°. Let's see what doing hue rotations on this color look like: #+begin_src elisp :results raw :exports results (apply 'ns/blog-make-color-strip (-unzip (let ((rot 3)) (-map (fn (list (ct-make-hsl (+ 19.6 (* <> (/ 360 rot))) 19.59832834874923 42.52063782711528) (format "%s" (+ 19.6 (* <> (/ 360 rot)))) ;; 19.59832834874923 42.52063782711528 )) (range rot))))) #+end_src {{{detail(HSL: 120° rotation (hue value shown))}}} #+begin_src elisp :results raw :exports results (apply 'ns/blog-make-color-strip (-unzip (let ((rot 4)) (-map (fn (list (ct-make-hsl (+ 19.6 (* <> (/ 360 rot))) 19.59832834874923 42.52063782711528) (format "%s" (+ 19.6 (* <> (/ 360 rot)))) ;; 19.59832834874923 42.52063782711528 )) (range rot))))) #+end_src {{{detail(HSL: 90° rotation (hue value shown))}}} #+begin_src elisp :results raw :exports results (apply 'ns/blog-make-color-strip (-unzip (let ((rot 5)) (-map (fn (list (ct-make-hsl (+ 19.6 (* <> (/ 360 rot))) 19.59832834874923 42.52063782711528) (format "%s" (+ 19.6 (* <> (/ 360 rot)))) ;; 19.59832834874923 42.52063782711528 )) (range rot))))) #+end_src {{{detail(HSL: 72° rotation (hue value shown))}}} #+begin_src elisp :results raw :exports results (apply 'ns/blog-make-color-strip (-unzip (let ((rot 6)) (-map (fn (list (ct-make-hsl (+ 19.6 (* <> (/ 360 rot))) 19.59832834874923 42.52063782711528) (format "%s" (+ 19.6 (* <> (/ 360 rot)))) ;; 19.59832834874923 42.52063782711528 )) (range rot))))) #+end_src {{{detail(HSL: 60° rotation (hue value shown))}}} #+begin_src elisp :results raw :exports results (apply 'ns/blog-make-color-strip (-unzip (-take 6 (let ((rot 8)) (-map (fn (list (ct-make-hsl (+ 19.6 (* <> (/ 360 rot))) 19.59832834874923 42.52063782711528) (format "%s" (+ 19.6 (* <> (/ 360 rot)))) )) (range rot)))))) #+end_src {{{detail(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. *** [[#h-f23b8fe5-37a3-4ead-9d9d-a7139f76d532][white-point adjustment]] :PROPERTIES: :CUSTOM_ID: h-f23b8fe5-37a3-4ead-9d9d-a7139f76d532 :END: 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): #+begin_src elisp :results raw :exports results (let* ((word "AESTHETICS") (colors (ct-gradient (length word) (ht-get ns/theme :foreground) (ht-get ns/theme :background) t))) (ns/blog-make-color-strip colors (-map 'string word))) #+end_src {{{detail(original)}}} #+begin_src elisp :results raw :exports results (let* ((word "AESTHETICS") (colors (-map (lambda (c) (ct-lab-change-whitepoint c color-d65-xyz color-d50-xyz)) (ct-gradient (length word) (ht-get ns/theme :foreground) (ht-get ns/theme :background) t) ))) (ns/blog-make-color-strip colors (-map 'string word))) #+end_src {{{detail(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. ** [[#h-cb3c6479-7d62-4028-8942-2b033bb1247a][Implementing helpers]] :PROPERTIES: :CUSTOM_ID: h-cb3c6479-7d62-4028-8942-2b033bb1247a :END: This section is about the tools I implemented and use to actually do the thing™. Update <2021-01-22> I have packaged my helpers into an emacs package: [[https://github.com/neeasade/ct.el][ct.el]] Emacs ships with a fair amount of [[https://github.com/emacs-mirror/emacs/blob/master/lisp/color.el][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 [[https://github.com/neeasade/color-tools.el][wrappers]] that would do the conversion to your space of choice (coming from the 'name', strings eg "{{{color(#c930e8)}}}"). Here's an example -- say you wanted to increase luminosity of that color by a multiplier ~1.5~: #+begin_src emacs-lisp (ct-edit-hsl "#c930e8" (lambda (H S L) (list H S (* 1.5 L)))) ;; => "#eb16af59f708" #+end_src {{{color(#eb16af59f708)}}} is definitely a lighter color, nice. #+begin_quote 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. #+end_quote I also implemented a function for comparing contrast, referencing Peter Occil's wonderful [[https://peteroupc.github.io/colorgen.html][color notes]]: #+begin_src emacs-lisp ;; order does not matter: (ct-contrast-ratio "#ffffff" "#445544") ;; => 3.0000000000000004 #+end_src Is a color light? just check the lightness value in LAB space (note: that 65 value is \tilde{}opinions~): #+begin_src emacs-lisp (defun ct-is-light-p (name) (> (first (ct-name-to-lab name)) 65)) #+end_src A neat trick you can do with this is decide whether or not to use a dark or light foreground against the color: #+begin_src elisp :results raw :exports results (apply 'ns/blog-make-color-strip (-unzip (-map (lambda (c) (list c (if (ct-is-light-p c) "light" "dark"))) '("#006d77" "#83c5be" "#429958" "#edf6f9" "#ffddd2")))) #+end_src 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 {{{color(#000000)}}}!) #+begin_src emacs-lisp (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) #+end_src ** [[#h-0942db07-512b-45d6-8fd2-f3a641379b66][Vision quest]] :PROPERTIES: :CUSTOM_ID: h-0942db07-512b-45d6-8fd2-f3a641379b66 :END: 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 Sun 09:45> You can now see where I'm tracking these ideas in my [[https://github.com/neeasade/tarps#fishing][tarps]] repo. *** [[#h-009a56eb-e157-4ca0-bbe2-cbc00c2e6e20][Methods]] :PROPERTIES: :CUSTOM_ID: h-009a56eb-e157-4ca0-bbe2-cbc00c2e6e20 :END: 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: #+begin_src elisp :results raw :exports results (s-join "\n" (-map (lambda (items) (apply 'ns/blog-make-color-strip (-unzip items))) '( (("#e5e7ea" ":background") ("#e0dadb" ":background_") ("#d1c8ca" ":background__") ("#aacde6" ":background+")) (("#393a3c" ":foreground") ("#656669" ":foreground_") ("#393a3c" ":foreground+")) (("#2d249f" ":accent1") ("#0061c4" ":accent1_") ("#006e96" ":accent2") ("#2c7600" ":accent2_"))))) #+end_src Which in action looks like (click to see full size): {{{image(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. *** [[#h-9ba33a22-a924-4f8d-b27b-0e86b582b418][Bootstrapping]] :PROPERTIES: :CUSTOM_ID: h-9ba33a22-a924-4f8d-b27b-0e86b582b418 :END: 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. | 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 [[https://github.com/neeasade/emacs.d/blob/08526e0c49be60e8241005d39c8e4303ab4e6fd8/lisp/trees/style.el#L60-L129][some tweaks]] after the fact. Now that the mapping has been created, with some glue I can use any of the [[https://github.com/chriskempson/base16#builder-repositories][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. #+begin_src elisp :results raw :exports results (let* ((word "Happy Coloring!") (colors (ct-gradient (length word) (ht-get ns/theme :background) (ht-get ns/theme :foreground) t))) (ns/blog-make-color-strip colors (-map 'string word))) #+end_src *** [[#h-8f501cbc-6314-41f5-8cc1-054bd2b2fcfe][References and further reading]] :PROPERTIES: :CUSTOM_ID: h-8f501cbc-6314-41f5-8cc1-054bd2b2fcfe :END: - [[http://colorizer.org/][colorizer, an interactive color tool]] - [[https://peteroupc.github.io/colorgen.html][Peter Occil's "Color Topics for Programmers"]] - [[https://github.com/neeasade/color-tools.el][color-tools.el, my color utilities for emacs]] - [[https://github.com/neeasade/tarps][tarps, my base16 bootstrapping emacs themes]] - [[https://www.w3.org/TR/WCAG20/#relativeluminancedef][WCAG: Luminance]] - [[https://www.24a11y.com/2019/color-theory-and-contrast-ratios/][color theory and contrast ratios]] - [[https://en.wikipedia.org/wiki/CIELAB_color_space][wiki: CIELAB]] - [[https://en.wikipedia.org/wiki/SRGB][wiki: sRGB]] *** [[#h-faae9227-91c4-4c1c-a14a-5876d76c0a07][Thanks]] :PROPERTIES: :CUSTOM_ID: h-faae9227-91c4-4c1c-a14a-5876d76c0a07 :END: [[http://chriskempson.com/][Chris Kempson]] for the base16 project Shoutout to [[https://github.com/belak][belak]] for work on the the base16 emacs theme builder Thanks to the [[https://github.com/gnotclub/][Axis of Eval]], [[https://catgirl.sh/][camille]], and [[http://xero.nu/][xero]] for all their feedback when I was posting way too many pictures of colors.