You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

6.6 KiB

draft title aliases date series series_order author authorTwitter cover tags keywords summary showFullContent
true Deterministically Mapping Strings to RGB Colors
Deterministically Mapping Strings to RGB Colors
2025-01-25
strings-to-colors
1 Nick Dumas
golang
golang
Using Go so I don't have to think about color palettes. false

Inspiration

The XMPP site has a document describing a process for deterministically mapping strings to colors with XEP-0392. I've had this document saved in my notes for two years now and I haven't really had a good excuse to apply it until now.

I won't get too detailed here but the short version is

  • I've got a spreadsheet of addresses.
  • Each entry has a category and I'm turning that spreadsheet into a KML document.
  • I want each KML placemark to be colored according to its category but I don't want to manually assign colors to categories. I don't have much of an eye for design and categories might change over time.

All of this adds up to finding a way to programmatically turn a string into a color.

The Plan

I made an attempt at implementing XEP-0392 verbatim but I got walled by two details:

  1. Go doesn't have any HSL handling in the standard library.
  2. I don't understand HSLuv well enough to parse the instructions for converting the SHA1 hash to a color. HSL is comprised of 3 components ( hue, saturation, and lightness ) but the XEP-0392 document only talks about generating an angle.

I do understand RGB and how hashes work so I was able to synthesize that into a close-enough solution:

  • Take an arbitrary UTF-8 string
  • SHA1 the string
  • Interpret equally sized chunks of the hash as unsigned 64-bit integers: 3 chunks for RGB, 4 chunks for RGBA.
  • Downscale those integers into 8-bit unsigned integers.

The Implementation

ToRGB is pretty straightforward, implementing the algorithm described in the previous section. We take the SHA1 of the string, break it into chunks, convert those into uint64s to account for the 7 bytes ( 56 bits total ) we're turning into an integer. The blue value overlaps the green by a few bits but that this isn't a security system so that doesn't matter much.

func ToRGB(s string) color.RGBA {
        h := sha1.New()
        io.WriteString(h, s)
        sum := h.Sum(nil)
        var rgb color.RGBA
        r := binary.LittleEndian.Uint64(sum[:8])
        g := binary.LittleEndian.Uint64(sum[8:16])
        b := binary.LittleEndian.Uint64(sum[12:])
        rgb.R = uint8(InterpolateUint64(r, 0, uint64(math.Pow(2, 56)), 0, 64384))
        rgb.G = uint8(InterpolateUint64(g, 0, uint64(math.Pow(2, 56)), 0, 64384))
        rgb.B = uint8(InterpolateUint64(b, 0, uint64(math.Pow(2, 56)), 0, 64384))
        rgb.A = uint8(255)

        return rgb
}

ToRGBA functions almost identically, with a few small changes. Instead of 7 byte chunks, we use 5 byte chunks and adjust the Interpolate call accordingly for the 40 bit chunks.

func ToRGBA(s string) color.RGBA {
// ...
		a := binary.LittleEndian.Uint64(sum[15:])
// ...
		rgba.A = uint8(InterpolateUint64(a, 0, uint64(math.Pow(2, 40)), 0, 64384))
// ...
		return rgb
}

Interpolation

  • Not sure if interpolation is the right technical term
  • Using generics
func Interpolate[T constraints.Unsigned](f, inputMin, inputMax, outputMin, outputMax T) T {
        return (f-(inputMin))*(outputMax-outputMin)/(inputMax-inputMin) + outputMin
}

The Results

Below is a sample of "lorem ipsum" with each word colored using this algorithm.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Possible Improvements

  • Provide HSV or HSL color values
  • Light/dark mode awareness
  • Accessibility. The colors chosen are effectively random with no regard for color schemes, background, display device capability