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.

85 lines
6.6 KiB
Markdown

---
draft: true
title: "Deterministically Mapping Strings to RGB Colors"
aliases: ["Deterministically Mapping Strings to RGB Colors"]
date: "2025-01-25"
series: ["strings-to-colors"]
series_order: 1
author: "Nick Dumas"
authorTwitter: ""
cover: ""
tags: ["golang"]
keywords: ["golang"]
summary: "Using Go so I don't have to think about color palettes."
showFullContent: false
---
## Inspiration
The XMPP site has a document describing a process for deterministically mapping strings to colors with [XEP-0392](https://xmpp.org/extensions/xep-0392.html#algorithm-genpalette). 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:
4 weeks ago
- 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 `uint64`s 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.
```go
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.
```go
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
```go
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.
4 weeks ago
<p><span style="color:#FD9FAB;">Lorem</span> <span style="color:#D35EC5;">ipsum</span> <span style="color:#45EEDD;">dolor</span> <span style="color:#3A2FE8;">sit</span> <span style="color:#04B55A;">amet</span>, <span style="color:#883581;">consectetur</span> <span style="color:#31CA93;">adipiscing</span> <span style="color:#99461B;">elit</span>, <span style="color:#000AF1;">sed</span> <span style="color:#899099;">do</span> <span style="color:#7B05AB;">eiusmod</span> <span style="color:#F0B186;">tempor</span> <span style="color:#3D1519;">incididunt</span> <span style="color:#93B1CD;">ut</span> <span style="color:#8333C5;">labore</span> <span style="color:#846002;">et</span> <span style="color:#81BB0B;">dolore</span> <span style="color:#E3DDD1;">magna</span> <span style="color:#877C09;">aliqua</span>. <span style="color:#CB6967;">Ut</span> <span style="color:#B38347;">enim</span> <span style="color:#4B8A53;">ad</span> <span style="color:#078AC6;">minim</span> <span style="color:#D2F7E3;">veniam</span>, <span style="color:#6BD5A2;">quis</span> <span style="color:#2B37D2;">nostrud</span> <span style="color:#C592D6;">exercitation</span> <span style="color:#0EB9D1;">ullamco</span> <span style="color:#9BC026;">laboris</span> <span style="color:#872CC2;">nisi</span> <span style="color:#93B1CD;">ut</span> <span style="color:#7DB82B;">aliquip</span> <span style="color:#1CED95;">ex</span> <span style="color:#C0504C;">ea</span> <span style="color:#5DA8B5;">commodo</span> <span style="color:#B762A6;">consequat</span>. <span style="color:#2FCB06;">Duis</span> <span style="color:#EADF81;">aute</span> <span style="color:#0602B8;">irure</span> <span style="color:#45EEDD;">dolor</span> <span style="color:#5E0D47;">in</span> <span style="color:#8B2DEF;">reprehenderit</span> <span style="color:#5E0D47;">in</span> <span style="color:#790B62;">voluptate</span> <span style="color:#7486E8;">velit</span> <span style="color:#33E7F2;">esse</span> <span style="color:#DE099F;">cillum</span> <span style="color:#81BB0B;">dolore</span> <span style="color:#C514CD;">eu</span> <span style="color:#B4B15A;">fugiat</span> <span style="color:#93AA9C;">nulla</span> <span style="color:#83967C;">pariatur</span>. <span style="color:#EDD318;">Excepteur</span> <span style="color:#580A2A;">sint</span> <span style="color:#7DC184;">occaecat</span> <span style="color:#BCEEAB;">cupidatat</span> <span style="color:#EA0503;">non</span> <span style="color:#0AC6DC;">proident</span>, <span style="color:#90C0A3;">sunt</span> <span style="color:#5E0D47;">in</span> <span style="color:#0C0B4B;">culpa</span> <span style="color:#B0E1D5;">qui</span> <span style="color:#F8F0B6;">officia</span> <span style="color:#FFF0B6;">deserunt</span> <span style="color:#115AE8;">mollit</span> <span style="color:#6EC31F;">anim</span> <span style="color:#10B00E;">id</span> <span style="color:#8ADFA4;">est</span> <span style="color:#7EBB61;">laborum</span>. </p>
4 weeks ago
## Possible Improvements
4 weeks ago
- 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