(or emacs irrelevant)

Blending colors in Elisp

Intro

This is a slightly obscure feature that I've added today to ivy-mode. It's obscure, since very few packages give their candidates a face with a custom background. Still, when that happens, a conflict arises:

Which face to apply: ivy-current-match or the existing one?

In the image below, I've blended them both in a 50-50 mixture:

ivy-blend.png

By the way, if you're wondering where the backgrounds come from in the first place, it's lispy-goto function (if you know what imenu is, it's an advanced version of that). This function collects all the Elisp tags in the current directory, and offers to jump to them. The functions that have interactive in them, are highlighted with lispy-command-name-face. In the above screenshot, the first 7 functions are interactive, and the other 2 are not.

If you're interested in how color blending is done, I'll describe it shortly below.

Color-building internals

First, a smaller and more clear example (I've used rainbow-mode and htmlize-buffer):

(colir-blend
 (color-values "red")
 (color-values "blue"))
;; => "#800080"

Here, colir-blend is a very small function that I'll describe below, and color-values is a function from faces.el that forwards to a C function xw-color-values with this doc:

Return a list of three integers, (RED GREEN BLUE), each between 0 and either 65280 or 65535 (the maximum depends on the system).

Which is a bit weird, since the last time I checked, the standard for colors was one-byte per channel, not two. Granted, it was about 10 years ago that I checked, and indeed, the entry on Wikipedia says:

High-end digital image equipment are often able to deal with larger integer ranges for each primary color, such as 0..1023 (10 bits), 0..65535 (16 bits) or even larger, by extending the 24-bits (three 8-bit values) to 32-bit, 48-bit, or 64-bit units

Apparently, Emacs is outfitted to deal high-end digital image equipment. But I'm still pretty sure that most graphics cards that you can buy only give 8 bits per each channel. Anyway, here's the code for the basic functions:

(defun colir-join (r g b)
  "Build a color from R G B.
Inverse of `color-values'."
  (format "#%02x%02x%02x"
          (ash r -8)
          (ash g -8)
          (ash b -8)))

(defun colir-blend (c1 c2 &optional alpha)
  "Blend the two colors C1 and C2 with ALPHA.
C1 and C2 are in the format of `color-values'.
ALPHA is a number between 0.0 and 1.0 which corresponds to the
influence of C1 on the result."
  (setq alpha (or alpha 0.5))
  (apply #'colir-join
         (cl-mapcar
          (lambda (x y)
            (round (+ (* x alpha) (* y (- 1 alpha)))))
          c1 c2)))

All pretty simple:

  1. Split each color into red, green, and blue integers with color-values.
  2. Compute a mean for each color channel.
  3. Build the new color by concatenating the hex components into a string.

In the example above:

  • The result of (color-values "red") is (65535 0 0).
  • The result of (color-values "blue") is (0 0 65535).
  • Their arithmetic mean is (32768 0 32768).
  • Divided by 256 with ash, we get (128 0 128).
  • Converted to hex, (128 0 128) is (80 0 80).
  • And the final color is "#800080".

Outro

I hope that you've enjoyed this little venture into Elisp and colors. People say rough things about Elisp sometimes, but hey: it offers functions ranging from the assembly-level ash, all the way up to Photoshop-grade color channel handling.

If only it could get a good optimization bump, like JavaScript got at one point, and a small threading library, we'd be all set.