From 0633afddf602a39e1c64d8e3c1161e0855748f8b Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 25 Jan 2025 15:04:29 -0500 Subject: [PATCH 01/15] drafting string2color post --- .../mapping-strings-to-rgb-colors/_index.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 content/posts/mapping-strings-to-rgb-colors/_index.md diff --git a/content/posts/mapping-strings-to-rgb-colors/_index.md b/content/posts/mapping-strings-to-rgb-colors/_index.md new file mode 100644 index 0000000..edba8b3 --- /dev/null +++ b/content/posts/mapping-strings-to-rgb-colors/_index.md @@ -0,0 +1,22 @@ +--- +draft: true +title: "Deterministically Mapping Strings to RGB Colors" +aliases: ["Deterministically Mapping Strings to RGB Colors"] +date: "2025-1-25" +series: ["strings-to-colors"] +series_order: 0 +author: "Nick Dumas" +authorTwitter: "" +cover: "" +tags: ["golang"] +keywords: ["golang"] +summary: "Using Go so I don't have to think about color palettes." +showFullContent: false +--- +{{< lead >}} {{ }} + +## The Plan +- +## The Implementation +## The Results +## Possible Improvements From 8452b13cb4142a67851e8e90e456fc6c666e6408 Mon Sep 17 00:00:00 2001 From: Nick Dumas Date: Wed, 23 Oct 2024 08:42:51 -0400 Subject: [PATCH 02/15] Add resume to menu --- config/_default/menus.en.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/_default/menus.en.toml b/config/_default/menus.en.toml index 146b1b4..4433ff8 100644 --- a/config/_default/menus.en.toml +++ b/config/_default/menus.en.toml @@ -51,6 +51,12 @@ url = "https://fosstodon.org/@nickdumas" weight = 10 +[[main]] + name = "Resume" + pre = "" + pageRef = "/resume.pdf" + weight = 25 + [[main]] name = "Office Hours" pageRef = "services/office-hours" From 350d0830f8710bbcd897dddfdecabe368af993ea Mon Sep 17 00:00:00 2001 From: Nick Dumas Date: Wed, 23 Oct 2024 08:48:39 -0400 Subject: [PATCH 03/15] maybe it needs to be relative instead of absolute? --- config/_default/menus.en.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/_default/menus.en.toml b/config/_default/menus.en.toml index 4433ff8..8540dff 100644 --- a/config/_default/menus.en.toml +++ b/config/_default/menus.en.toml @@ -54,7 +54,7 @@ [[main]] name = "Resume" pre = "" - pageRef = "/resume.pdf" + pageRef = "resume.pdf" weight = 25 [[main]] From 2f0b4e34532428edb5d31c1312e2e0def29cc9f2 Mon Sep 17 00:00:00 2001 From: Nick Dumas Date: Wed, 23 Oct 2024 09:08:20 -0400 Subject: [PATCH 04/15] Fine, I'll use an absolute URL --- config/_default/menus.en.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/_default/menus.en.toml b/config/_default/menus.en.toml index 8540dff..fe45a4b 100644 --- a/config/_default/menus.en.toml +++ b/config/_default/menus.en.toml @@ -52,9 +52,9 @@ weight = 10 [[main]] - name = "Resume" + name = "Resumé" pre = "" - pageRef = "resume.pdf" + url = "https://ndumas.com/resume.pdf" weight = 25 [[main]] From 264367cef14bcb4f17c143b2377affe1cfd8680f Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 25 Jan 2025 15:30:29 -0500 Subject: [PATCH 05/15] more drafting --- .../posts/mapping-strings-to-rgb-colors/_index.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/content/posts/mapping-strings-to-rgb-colors/_index.md b/content/posts/mapping-strings-to-rgb-colors/_index.md index edba8b3..aee218f 100644 --- a/content/posts/mapping-strings-to-rgb-colors/_index.md +++ b/content/posts/mapping-strings-to-rgb-colors/_index.md @@ -16,7 +16,20 @@ showFullContent: false {{< lead >}} {{ }} ## The Plan -- +- Take an arbitrary UTF-8 string +- Generate an RGB or RGBA color code for it in a deterministic way + - SHA1 the string + - Interpret equally sized chunks of the hash as integers + - Scale those integers into an 8-bit space ## The Implementation ## The Results +Below is a sample of the "lorum ipsum" text with each word colored using this algorithm. + +

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

+ + + ## 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 From ad7daaf53be028de79f51716d063fc84aa9e58f4 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 25 Jan 2025 19:37:06 -0500 Subject: [PATCH 06/15] Renaming index file --- .../{_index.md => index.md} | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) rename content/posts/mapping-strings-to-rgb-colors/{_index.md => index.md} (77%) diff --git a/content/posts/mapping-strings-to-rgb-colors/_index.md b/content/posts/mapping-strings-to-rgb-colors/index.md similarity index 77% rename from content/posts/mapping-strings-to-rgb-colors/_index.md rename to content/posts/mapping-strings-to-rgb-colors/index.md index aee218f..6fa8d75 100644 --- a/content/posts/mapping-strings-to-rgb-colors/_index.md +++ b/content/posts/mapping-strings-to-rgb-colors/index.md @@ -20,14 +20,22 @@ showFullContent: false - Generate an RGB or RGBA color code for it in a deterministic way - SHA1 the string - Interpret equally sized chunks of the hash as integers - - Scale those integers into an 8-bit space + - Scale those integers into an 8-bit unsigned space ## The Implementation +### Hashing +{{< highlight go > }} +{{ }} +### Interpolation +- Not sure if interpolation is the right technical term +- Using generics +{{< highlight go > }} +{{ }} ## The Results -Below is a sample of the "lorum ipsum" text with each word colored using this algorithm. +Below is a sample of "lorem ipsum" with each word colored using this algorithm.

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

- +It doesn't look too bad. ## Possible Improvements - Provide HSV or HSL color values From 12e42dacb80d9e42715b9158e67480b5444c37dd Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 25 Jan 2025 19:38:50 -0500 Subject: [PATCH 07/15] I think hugo hates these spaces --- content/posts/mapping-strings-to-rgb-colors/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/content/posts/mapping-strings-to-rgb-colors/index.md b/content/posts/mapping-strings-to-rgb-colors/index.md index 6fa8d75..2680ae8 100644 --- a/content/posts/mapping-strings-to-rgb-colors/index.md +++ b/content/posts/mapping-strings-to-rgb-colors/index.md @@ -23,13 +23,13 @@ showFullContent: false - Scale those integers into an 8-bit unsigned space ## The Implementation ### Hashing -{{< highlight go > }} -{{ }} +{{< highlight go >}} +{{}} ### Interpolation - Not sure if interpolation is the right technical term - Using generics -{{< highlight go > }} -{{ }} +{{< highlight go >}} +{{}} ## The Results Below is a sample of "lorem ipsum" with each word colored using this algorithm. From 175baa8a0e2723289e5363ddcbe89e0b375ee8d0 Mon Sep 17 00:00:00 2001 From: Nick Dumas Date: Fri, 25 Oct 2024 07:48:11 -0400 Subject: [PATCH 08/15] Putting mentoring on hold --- config/_default/menus.en.toml | 8 ++++---- content/_index.md | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/config/_default/menus.en.toml b/config/_default/menus.en.toml index fe45a4b..c1d0524 100644 --- a/config/_default/menus.en.toml +++ b/config/_default/menus.en.toml @@ -62,10 +62,10 @@ pageRef = "services/office-hours" weight = 60 -[[main]] - name = "Personal Mentoring" - pageRef = "services/mentoring" - weight = 50 +# [[main]] + # name = "Personal Mentoring" + # pageRef = "services/mentoring" + # weight = 50 #[[main]] # name = "Parent" # weight = 20 diff --git a/content/_index.md b/content/_index.md index 8b45504..b118beb 100644 --- a/content/_index.md +++ b/content/_index.md @@ -10,8 +10,6 @@ Hi, my name's Nick, he/him. I'm a software developer, infrastructure engineer, a I do a little [drawing](https://www.artstation.com/nickdumas) now and then but my big artistic focuses are music and tabletop gaming with an emphasis on systems that decrease the power imbalance between the facilitator and players. I make weird abstract tracks with [bespoke synth](https://www.bespokesynth.com/) and have a weekly game session where we try out new game systems and regularly rotate through who's running what. -I also offer [mentoring](/services/mentoring/) services for programming, note-taking, and general problem-solving. - ## My Credentials In no particular order I've worked on - Python set-top box and browser frontends for OTT ( over the topology ) media delivery networks. From 68409e2863e09373dea9b9b22b611ef8ca8ac7e3 Mon Sep 17 00:00:00 2001 From: Nick Dumas Date: Sun, 26 Jan 2025 00:13:32 -0500 Subject: [PATCH 09/15] Pushing this image to my personal OCI registry --- .drone.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.drone.yml b/.drone.yml index 3110dcf..a3525f7 100644 --- a/.drone.yml +++ b/.drone.yml @@ -13,7 +13,7 @@ steps: - name: publish_prod depends_on: - hugo - image: drillster/drone-rsync + image: code.ndumas.com/ndumas/drone-rsync settings: key: from_secret: BLOG_DEPLOY_KEY @@ -31,7 +31,7 @@ steps: - name: publish_dev depends_on: - hugo - image: drillster/drone-rsync + image: code.ndumas.com/ndumas/drone-rsync settings: key: from_secret: BLOG_DEPLOY_KEY @@ -49,7 +49,7 @@ steps: - name: publish_drafts depends_on: - hugo - image: drillster/drone-rsync + image: code.ndumas.com/ndumas/drone-rsync settings: key: from_secret: BLOG_DEPLOY_KEY From 60a94a3c16d3b006f34fb810de63632378c68a66 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 26 Jan 2025 00:21:55 -0500 Subject: [PATCH 10/15] Do I need double digit months? --- content/posts/mapping-strings-to-rgb-colors/index.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/content/posts/mapping-strings-to-rgb-colors/index.md b/content/posts/mapping-strings-to-rgb-colors/index.md index 2680ae8..f723240 100644 --- a/content/posts/mapping-strings-to-rgb-colors/index.md +++ b/content/posts/mapping-strings-to-rgb-colors/index.md @@ -2,7 +2,7 @@ draft: true title: "Deterministically Mapping Strings to RGB Colors" aliases: ["Deterministically Mapping Strings to RGB Colors"] -date: "2025-1-25" +date: "2025-01-25" series: ["strings-to-colors"] series_order: 0 author: "Nick Dumas" @@ -15,6 +15,8 @@ showFullContent: false --- {{< lead >}} {{ }} +## Inspiration +The XMPP site has a document describing https://xmpp.org/extensions/xep-0392.html#algorithm-genpalette ## The Plan - Take an arbitrary UTF-8 string - Generate an RGB or RGBA color code for it in a deterministic way From 8c65dd4224cd84c53356e0c7d04e53b5f3b9835a Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 26 Jan 2025 00:27:31 -0500 Subject: [PATCH 11/15] Gotta get this heading right --- content/posts/mapping-strings-to-rgb-colors/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/content/posts/mapping-strings-to-rgb-colors/index.md b/content/posts/mapping-strings-to-rgb-colors/index.md index f723240..1fa7d34 100644 --- a/content/posts/mapping-strings-to-rgb-colors/index.md +++ b/content/posts/mapping-strings-to-rgb-colors/index.md @@ -27,11 +27,13 @@ The XMPP site has a document describing https://xmpp.org/extensions/xep-0392.htm ### Hashing {{< highlight go >}} {{}} + ### Interpolation - Not sure if interpolation is the right technical term - Using generics {{< highlight go >}} {{}} + ## The Results Below is a sample of "lorem ipsum" with each word colored using this algorithm. From e086ccea3584c732a04cab959a293bb2b5af8f96 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 26 Jan 2025 00:28:16 -0500 Subject: [PATCH 12/15] Series are not zero-indexed --- content/posts/mapping-strings-to-rgb-colors/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/posts/mapping-strings-to-rgb-colors/index.md b/content/posts/mapping-strings-to-rgb-colors/index.md index 1fa7d34..0e068a0 100644 --- a/content/posts/mapping-strings-to-rgb-colors/index.md +++ b/content/posts/mapping-strings-to-rgb-colors/index.md @@ -4,7 +4,7 @@ title: "Deterministically Mapping Strings to RGB Colors" aliases: ["Deterministically Mapping Strings to RGB Colors"] date: "2025-01-25" series: ["strings-to-colors"] -series_order: 0 +series_order: 1 author: "Nick Dumas" authorTwitter: "" cover: "" From 87d9a2cbd98d63ce004b5d883ba0a10fe4198c8c Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 26 Jan 2025 19:12:00 -0500 Subject: [PATCH 13/15] Fleshing out the draft --- .../mapping-strings-to-rgb-colors/index.md | 67 ++++++++++++++----- 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/content/posts/mapping-strings-to-rgb-colors/index.md b/content/posts/mapping-strings-to-rgb-colors/index.md index 0e068a0..6c9c69a 100644 --- a/content/posts/mapping-strings-to-rgb-colors/index.md +++ b/content/posts/mapping-strings-to-rgb-colors/index.md @@ -13,33 +13,70 @@ keywords: ["golang"] summary: "Using Go so I don't have to think about color palettes." showFullContent: false --- -{{< lead >}} {{ }} - ## Inspiration -The XMPP site has a document describing https://xmpp.org/extensions/xep-0392.html#algorithm-genpalette +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: - Take an arbitrary UTF-8 string -- Generate an RGB or RGBA color code for it in a deterministic way - - SHA1 the string - - Interpret equally sized chunks of the hash as integers - - Scale those integers into an 8-bit unsigned space +- 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 -### Hashing -{{< highlight go >}} -{{}} +`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 -{{< highlight go >}} -{{}} +```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. -

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

- -It doesn't look too bad. +

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 From 53fc1596890519b4c21d6c08e3890b222ebf6b17 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 28 Jan 2025 22:09:10 -0500 Subject: [PATCH 14/15] final draft --- .../mapping-strings-to-rgb-colors/index.md | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/content/posts/mapping-strings-to-rgb-colors/index.md b/content/posts/mapping-strings-to-rgb-colors/index.md index 6c9c69a..80d8dba 100644 --- a/content/posts/mapping-strings-to-rgb-colors/index.md +++ b/content/posts/mapping-strings-to-rgb-colors/index.md @@ -43,42 +43,48 @@ func ToRGB(s string) 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.R = uint8(ScaleUint64(r, 0, uint64(math.Pow(2, 56)), 0, 64384)) + rgb.G = uint8(ScaleUint64(g, 0, uint64(math.Pow(2, 56)), 0, 64384)) + rgb.B = uint8(ScaleUint64(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. +`ToRGBA` functions almost identically, with a few small changes. Instead of 7 byte chunks, we use 5 byte chunks and adjust the `Scale` call accordingly for the 40 bit chunks. ```go func ToRGBA(s string) color.RGBA { // ... - a := binary.LittleEndian.Uint64(sum[15:]) + // We use Uint32 here because the chunks are smaller and the Uint64 function panics if the input []byte is too short. + a := binary.LittleEndian.Uint32(sum[15:]) // ... - rgba.A = uint8(InterpolateUint64(a, 0, uint64(math.Pow(2, 40)), 0, 64384)) + rgba.A = uint8(ScaleUint64(a, 0, uint64(math.Pow(2, 40)), 0, 64384)) // ... return rgb } ``` -### Interpolation -- Not sure if interpolation is the right technical term -- Using generics +### Scaling +The final bit of magic we need is a way to scale these 64 bit integers down to between 0 and 255 to represent the RGB ( and sometimes A ) values that make up the `image.Color`. Enter the `Scale` function: + ```go -func Interpolate[T constraints.Unsigned](f, inputMin, inputMax, outputMin, outputMax T) T { +func Scale[T constraints.Unsigned | constraints.Float | constraints.Signed](f, inputMin, inputMax, outputMin, outputMax T) T { return (f-(inputMin))*(outputMax-outputMin)/(inputMax-inputMin) + outputMin } + +var ScaleUint64 = Scale[uint64] ``` +This does a bit of relatively straightforward math to smoothly scale the input into the desired range. Not a lot to say about it, it's just math and I don't know much about where it came from. I've had this floating around in various repos for years and I don't have any notes on where I found it. + ## 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 +There's a few places where this code could be meaningfully improved. +- It'd be nice to have HSL/HSV colors as an option. I've personally never been in a situation where RGB wasn't enough to do what I needed, but they exist for a reason. [HSLuv](https://www.hsluv.org/) claims to better capture colors as perceived by the human eye and that sounds pretty neat. +- Accessibility and consistency. Sometimes the color for a word will be almost identical to its background making portions of the text hard or impossible to read. It'd be cool to be able to limit color generation to values within certain ranges of a color palette. This is probably doable with another application of `Scale`. + From d222fe01d1bd15e983a9557311ad856634e90ed2 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 28 Jan 2025 22:09:22 -0500 Subject: [PATCH 15/15] Not a draft anymore --- content/posts/mapping-strings-to-rgb-colors/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/posts/mapping-strings-to-rgb-colors/index.md b/content/posts/mapping-strings-to-rgb-colors/index.md index 80d8dba..d582989 100644 --- a/content/posts/mapping-strings-to-rgb-colors/index.md +++ b/content/posts/mapping-strings-to-rgb-colors/index.md @@ -1,5 +1,5 @@ --- -draft: true +draft: false title: "Deterministically Mapping Strings to RGB Colors" aliases: ["Deterministically Mapping Strings to RGB Colors"] date: "2025-01-25"