A Weird Imagination

Better hash-based colors

The problem#

Yesterday, I proposed using a hash function to choose colors:

ps1_color="32;38;5;$((0x$(hostname | md5sum | cut -f1 -d' ' | tr -d '\n' | tail -c2)))"

I did not discuss two issues with this approach:

  1. Colors may be too similar to other colors, such that they are not useful.
  2. Some colors may be undesirable altogether, particularly very dark ones may be too similar to a black terminal background color.

What are color numbers?#

In order to handle these issues, color numbers cannot be treated as a black box. The color number for 256 color terminals in fact has structure. This visualization shows that structure quite clearly:

  1. The first 16 colors (0 through 15) are from 8/16 color terminals: the 8 basic colors and then their bright versions.

  2. The next 216 colors (16 through 231) are defined by a 6x6x6 RGB color cube. That means that the colors have red, green, and blue components, each of which are specified by a number 0 through 5: color=red⋅62 + green⋅61 + blue⋅60 + 16.

  3. The remaining 24 colors (232 through 255) are shades of gray.

A new structure#

Suppose we want to avoid dim colors and also not use any of the initial 16 colors. One way to do this would be to avoid all of the initial 16 colors and the 2 dimmest settings in each color dimension and the dimmest 8 grays. That is, each of red, green, and blue settings may never be 0 or 1. That defines a new structure which has a 4x4x4 RGB color cube (64 colors) followed by 16 shades of gray for a total of 80 colors.

Generating colors in this space is easy: just compute a hash modulo 80.

In order to actually use these colors, we need a way to map from this newly defined color space back to the 256 color space. Here's a script to do that mapping:

#!/bin/bash

incolor=$1
if [[ $incolor -ge 64 ]]
then
    outcolor=$((232 + incolor - 64 + 8))
else
    b=$((2+incolor%4))
    g=$((2+incolor/4%4))
    r=$((2+incolor/4/4%4))
    outcolor=$((16 + r*6*6 + g*6 + b))
fi

echo -e "\033[38;5;${outcolor}m"This is a test. outcolor=$outcolor"\033[0m"

First it checks if the color is in the gray range (≥64). If so, it converts the color to a 256 color gray by subtracting out the 64, adding in the 256 color gray offset of 232 and adding 8 to correspond to the fact that the dimmest 8 grays are not in our encoding. Otherwise, it takes apart the 4x4x4 color cube by using division and modulus, adds 2 to each part and puts it back together in the format for the 256 color 6x6x6 color cube.

Another structure you might use if you are worried about colors being too close to each other is one which does not generate adjacent colors. To get every other color, use a 3x3x3 color cube and 12 shades of gray for a total of 3⋅3⋅3+12=39 colors:

#!/bin/bash

incolor=$1
if [[ $incolor -ge 27 ]]
then
    outcolor=$((232+(incolor-27)*2+1))
else
    b=$((1+2*(incolor%3)))
    g=$((1+2*(incolor/3%3)))
    r=$((1+2*(incolor/3/3%3)))
    outcolor=$((16 + r*6*6 + g*6 + b))
fi

echo -e "\033[38;5;${outcolor}m"This is a test. outcolor=$outcolor"\033[0m"

Now we multiply the color numbers extracted from $incolor by 2 (and add 1 to get brighter colors) to map to colors which are not next to each other.

Color numbers from a hex hash#

Be careful of negative numbers when using bash to convert from hex:

$ echo $(( ( 0x$(echo foo | sha256sum | tr -d '\n -') ) % 80))
-68

One workaround is to simply remove the minus sign:

$ var=$(( ( 0x$(echo foo | sha256sum | tr -d '\n -') ) % 80))
$ echo ${var#-}
68

Another workaround is to use bc for the math instead of bash:

$ (echo ibase=16;
    echo -n a=;
    echo foo | sha256sum | tr -d ' -' | tr a-f A-F;
    echo ibase=10;
    echo a%80) | bc
76

That's using bash to write a bc program and then piping that program to bc for it to be executed. Remove the | bc to see what that program actually is:

$ echo ibase=16; echo -n a=; echo foo | sha256sum | tr -d ' -' | tr a-f A-F; echo ibase=10; echo a%80
ibase=16
a=B5BB9D8014A0F9B1D61E21E796D78DCCDF1352F23CD32812F4850B878AE4944C
ibase=10
a%80

The special ibase variable in bc controls the input base. We switch it to 16 to read the hex (note the use of tr to capitalize the hex digits because bc requires that) and then back to 10 because 80 is written in decimal.

Comments

Have something to add? Post a comment by sending an email to comments@aweirdimagination.net. You may use Markdown for formatting.

There are no comments yet.