6.6 KiB
draft | title | aliases | date | series | series_order | author | authorTwitter | cover | tags | keywords | summary | showFullContent | ||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
true | Deterministically Mapping Strings to RGB Colors |
|
2025-01-25 |
|
1 | Nick Dumas |
|
|
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:
- Go doesn't have any HSL handling in the standard library.
- 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 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.
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