UP | HOME

random foreground color generation

Table of Contents

1 Motivation

It's not that easy to generate readable random foreground colors for a given background, but it helps, say, to distinguish nicks in IRC. So, we need to figure out how to do that properly.

2 Approach

I've tried a few of them, and they are usually related to perceived brightness.1 Simple brightness check works, but is not perfect; CIE 1931 XYZ color space allows to generate colors with a given perceived brightness fast, but is not perfect too (one of the issues is pink, #FF00FF, on dark background: pink is bright, but is not readable still).

But there are Web Content Accessibility Guidelines, including a description of contrast ratio ensurance procedure, which seems very nice.

3 Generating appropriate colors

Now we'll write some code in Scheme.

3.1 Helper functions

We'll need some random and to work with numbers in \([0,1]\):

(define (rand)
  (/ (random 42001) 42000))

(define (nn n)
  (cond ((< n 0) 0)
        ((> n 1) 1)
        (else n)))

3.2 Ensurance procedure

So, we have this procedure, \(L = 0.2126R + 0.7152G + 0.0722B\), where \(R\), \(G\) and \(B\) are defined as:

  • if \(R_{sRGB} \le 0.03928\) then \(R = \frac{R_{sRGB}}{12.92}\) else \(R = \frac{R_{sRGB} +0.055}{1.055}^{2.4}\)
  • if \(G_{sRGB} \le 0.03928\) then \(G = \frac{G_{sRGB}}{12.92}\) else \(G = \frac{G_{sRGB} +0.055}{1.055}^{2.4}\)
  • if \(B_{sRGB} \le 0.03928\) then \(B = \frac{B_{sRGB}}{12.92}\) else \(B = \frac{B_{sRGB} +0.055}{1.055}^{2.4}\)

…and \(R_{sRGB}\), \(G_{sRGB}\) and \(B_{sRGB}\) are defined as:

  • \(R_{sRGB} = \frac{R_{8bit}}{255}\)
  • \(G_{sRGB} = \frac{G_{8bit}}{255}\)
  • \(B_{sRGB} = \frac{B_{8bit}}{255}\)

And its implementation is straightforward (assuming that we work only with sRGB here):

(define (c-to-l c)
  (if (<= c 0.03928)
      (/ c 12.92)
      (expt (/ (+ c 0.055) 1.055) 2.4)))

(define (relative-luminance rgb)
  (apply +
         (map (lambda (color coefficient)
                (* coefficient
                   (c-to-l color)))
              rgb
              '(0.2126 0.7152 0.0722))))

(define (contrast-ratio color1 color2)
  (let ((l1 (+ 0.05 (relative-luminance color1)))
        (l2 (+ 0.05 (relative-luminance color2))))
    (if (> l1 l2)
        (/ l1 l2)
        (/ l2 l1))))

3.3 Generation procedure

3.3.1 The l-to-c function

We could start with l-to-c (luminance-to-color, at least roughly) function, which is like c-to-l written backwards:

(define (l-to-c m)
  (if (<= m (/ 0.03928 12.92))
      (* m 12.92)
      (- (* (expt m (/ 1 2.4))
            1.055)
         0.055)))

3.3.2 Main part

Now, we need to generate actual colors, and will write two functions for that: one for colors with luminance higher than N, and another one for colors with luminance lower than N. First of them:

(define (color-with-luminance-higher-than N)
  (let* ((Rc 0.2126)
         (Gc 0.7152)
         (Bc 0.0722)

         (R-min-lum (nn (/ (- N Gc Bc) Rc)))
         (R-min-color (l-to-c R-min-lum))
         (R-color (+ R-min-color (* (rand) (- 1 R-min-color))))
         (R-lum (* Rc (c-to-l R-color)))

         (G-min-lum (nn (/ (- N R-lum Bc) Gc)))
         (G-min-color (l-to-c G-min-lum))
         (G-color (+ G-min-color (* (rand) (- 1 G-min-color))))
         (G-lum (* Gc (c-to-l G-color)))

         (B-min-lum (nn (/ (- N R-lum G-lum) Bc)))
         (B-min-color (l-to-c B-min-lum))
         (B-color (+ B-min-color (* (rand) (- 1 B-min-color))))
         (B-lum (* Bc (c-to-l B-color))))
    (list R-color G-color B-color)))

Here, we need to gain at least N luminance for the resulting color; therefore, for every component we should check its minimal luminance first. Red could give maximum of 0.2126 luminance to the resulting color, green - 0.7152, blue - 0.0722.

So, if we are generating red component first, its minimal luminance should be such that green and blue channels will be able to generate enough luminance in their parts too, which is calculated in R-min-lum.

Then, l-to-c gives us a color which will give that luminance, and we call it R-min-color.

Actual red color level is called R-color, and just should be in \([R_{min color}, 1]\).

And then we are calculating resulting luminance with original c-to-l function, to subtract it from N later: it's already gained, we don't need that part of N to generate appropriate green and blue levels.

The rest is similar. And there's also a similar function for generating colors with a lower luminance than N:

(define (color-with-luminance-lower-than N)
  (let* ((Rc 0.2126)
         (Gc 0.7152)
         (Bc 0.0722)

         (R-max-lum (nn (/ N Rc)))
         (R-max-color (l-to-c R-max-lum))
         (R-color (* R-max-color (rand)))
         (R-lum (* Rc (c-to-l R-color)))

         (G-max-lum (nn (/ (- N R-lum) Gc)))
         (G-max-color (l-to-c G-max-lum))
         (G-color (* G-max-color (rand)))
         (G-lum (* Gc (c-to-l G-color)))

         (B-max-lum (nn (/ (- N R-lum G-lum) Bc)))
         (B-max-color (l-to-c B-max-lum))
         (B-color (* B-max-color (rand)))
         (B-lum (* Bc (c-to-l B-color))))
    (list R-color G-color B-color)))

3.3.3 Final part

Finally, we should write the last function, generate-contrast-color. We only need to find \(X\) for a given \(Y\) in \(\frac{X + 0.05}{Y + 0.05} \gt 7\), if \(Y \lt \frac{1}{7} - 0.05\), or in \(\frac{Y + 0.05}{X + 0.05} \gt 7\) otherwise. That is, \(X \gt 7 (Y + 0.05) + 0.05\) or \(X \lt \frac{Y + 0.05}{7} - 0.05\):

(define (generate-contrast-color color ratio)
  (let ((color-lum (relative-luminance color)))
    (if (< color-lum (- (/ 1 ratio) 0.05))
        (color-with-luminance-higher-than (+ (* (+ color-lum 0.05) ratio) 0.05))
        (color-with-luminance-lower-than (- (/ (+ color-lum 0.05) ratio) 0.05)))))

4 Application

Initial goal was to improve Circe's color-nicks.el, so here is a translation into Emacs Lisp and examples:

4.1 Code

(defsubst circe-w3-contrast-c-to-l (c)
  (if (<= c 0.03928)
      (/ c 12.92)
    (expt (/ (+ c 0.055) 1.055) 2.4)))

(defsubst circe-w3-contrast-relative-luminance (rgb)
  (apply '+
         (cl-mapcar (lambda (color coefficient)
                      (* coefficient
                         (circe-w3-contrast-c-to-l color)))
                    rgb
                    '(0.2126 0.7152 0.0722))))

(defsubst circe-w3-contrast-contrast-ratio (color1 color2)
  (let ((l1 (+ 0.05 (circe-w3-contrast-relative-luminance color1)))
        (l2 (+ 0.05 (circe-w3-contrast-relative-luminance color2))))
    (if (> l1 l2)
        (/ l1 l2)
        (/ l2 l1))))

(defsubst circe-w3-contrast-rand ()
  (/ (random 42001) 42000.0))

(defsubst circe-w3-contrast-l-to-c (m)
  (if (<= m (/ 0.03928 12.92))
      (* m 12.92)
      (- (* (expt m (/ 1 2.4))
            1.055)
         0.055)))

(defsubst circe-w3-contrast-nn (n)
  (cond ((< n 0) 0)
        ((> n 1) 1)
        (t n)))

(defsubst circe-w3-contrast-color-with-luminance-higher-than (N)
  (let* ((Rc 0.2126)
         (Gc 0.7152)
         (Bc 0.0722)

         (R-min-lum (circe-w3-contrast-nn (/ (- N Gc Bc) Rc)))
         (R-min-color (circe-w3-contrast-l-to-c R-min-lum))
         (R-color (+ R-min-color (* (circe-w3-contrast-rand) (- 1 R-min-color))))
         (R-lum (* Rc (circe-w3-contrast-c-to-l R-color)))

         (G-min-lum (circe-w3-contrast-nn (/ (- N R-lum Bc) Gc)))
         (G-min-color (circe-w3-contrast-l-to-c G-min-lum))
         (G-color (+ G-min-color (* (circe-w3-contrast-rand) (- 1 G-min-color))))
         (G-lum (* Gc (circe-w3-contrast-c-to-l G-color)))

         (B-min-lum (circe-w3-contrast-nn (/ (- N R-lum G-lum) Bc)))
         (B-min-color (circe-w3-contrast-l-to-c B-min-lum))
         (B-color (+ B-min-color (* (circe-w3-contrast-rand) (- 1 B-min-color))))
         (B-lum (* Bc (circe-w3-contrast-c-to-l B-color))))
    (list R-color G-color B-color)))

(defsubst circe-w3-contrast-color-with-luminance-lower-than (N)
  (let* ((Rc 0.2126)
         (Gc 0.7152)
         (Bc 0.0722)

         (R-max-lum (circe-w3-contrast-nn (/ N Rc)))
         (R-max-color (circe-w3-contrast-l-to-c R-max-lum))
         (R-color (* R-max-color (circe-w3-contrast-rand)))
         (R-lum (* Rc (circe-w3-contrast-c-to-l R-color)))

         (G-max-lum (circe-w3-contrast-nn (/ (- N R-lum) Gc)))
         (G-max-color (circe-w3-contrast-l-to-c G-max-lum))
         (G-color (* G-max-color (circe-w3-contrast-rand)))
         (G-lum (* Gc (circe-w3-contrast-c-to-l G-color)))

         (B-max-lum (circe-w3-contrast-nn (/ (- N R-lum G-lum) Bc)))
         (B-max-color (circe-w3-contrast-l-to-c B-max-lum))
         (B-color (* B-max-color (circe-w3-contrast-rand)))
         (B-lum (* Bc (circe-w3-contrast-c-to-l B-color))))
    (list R-color G-color B-color)))

(defsubst circe-w3-contrast-generate-contrast-color (color ratio)
  (let ((color-lum (circe-w3-contrast-relative-luminance color)))
    (if (< color-lum (- (/ 1.0 ratio) 0.05))
        (circe-w3-contrast-color-with-luminance-higher-than (+ (* (+ color-lum 0.05) ratio) 0.05))
        (circe-w3-contrast-color-with-luminance-lower-than (- (/ (+ color-lum 0.05) ratio) 0.05)))))

To generate a foreground color, we could simply use something like this now:

(defsubst circe-color-from-values (values)
  (apply 'concat 
         (cons "#" 
               (mapcar (lambda (val) 
                         (format "%02x" 
                                 (* (cond ((< val 0) 0)
                                          ((> val 1) 1)
                                          (t val))
                                    255)))
                       values))))

(circe-color-from-values 
 (circe-w3-contrast-generate-contrast-color 
  (mapcar (lambda (x) (/ x 65535.0))
          (color-values (face-background 'default)))
  7))

4.2 Examples

Here are colorized nicks on light and on dark background.

Footnotes:

1

Green appears to be the brightest to our eyes, red - like 3.5 times darker, and blue - like 3 times darker than red.