String Color
A deterministic method for deriving a visually consistent, UI-optimized color from any string. Designed to produce colors that read well as text, backgrounds, borders, and indicator dots across dark, light, sepia, ivory, and grey themes. Simpler and more perceptually balanced than hash-RGB approaches (e.g. XEP-0392).
# String Color
Any UTF-8 string can be mapped to a unique color via a polynomial character-code hash.
## Hashing
1. Normalize the input: trim() and toUpperCase()
2. Return a neutral grey rgb(128, 128, 128) for empty strings
3. Compute a BigInt by summing character codes weighted by powers of 256:
number = Σ ( charCode(i) × 256ⁱ ) for i in [0, len)
4. Derive hue: hue = number mod 360
## HSV Parameters
S = 0.70
V = 0.70 if 32 ≤ hue ≤ 204 (warm yellows, greens, cyans — perceptually bright)
V = 0.96 if 216 ≤ hue ≤ 273 (blues — perceptually dark, need boosting)
V = 0.90 otherwise (reds, magentas, warm blues)
## HSV → RGB Conversion
Standard HSV-to-RGB via intermediate chroma values:
h = hue / 60
c = V × S
x = c × (1 − |( h mod 2 ) − 1|)
m = V − c
Sector assignment (h in [0,6)):
| h range | R | G | B |
|———|––|––|—–|
| [0, 1) | c | x | 0 |
| [1, 2) | x | c | 0 |
| [2, 3) | 0 | c | x |
| [3, 4) | 0 | x | c |
| [4, 5) | x | 0 | c |
| [5, 6) | c | 0 | x |
Final output: R, G, B = round((r + m) × 255) for each channel.
# Hex String Color
When the input is a hexadecimal string (e.g. a pubkey of a Nostr
Profile
), parse it directly as a BigInt rather than hashing character codes:
number = BigInt.parse(hex, radix: 16)
hue = number mod 360
Then apply the same HSV conversion with one difference — V for the warm/green/cyan range is 0.75 instead of 0.70, which accounts for the higher average luminance of hex-encoded keys in that range:
V = 0.75 if 32 ≤ hue ≤ 204
V = 0.96 if 216 ≤ hue ≤ 273
V = 0.90 otherwise
# Text Readability Adjustment
When rendering the derived color as text (author names, mentions, etc.), apply a small brightness correction to the RGB output:
- Dark mode: multiply each channel by 1.08 (+8%)
- Light mode: multiply each channel by 0.95 (−5%)
Clamp each channel to [0, 255].
# Client Recommendations
- MUST normalize strings to trimmed uppercase before hashing
- MUST use BigInt arithmetic for the polynomial hash to avoid integer overflow
- SHOULD apply the text adjustment when rendering colored names or mentions
- MAY skip the text adjustment for non-text uses (backgrounds, borders, avatars)
- SHOULD fall back to rgb(128, 128, 128) for empty or invalid inputs
See Also
Comments
Public conversation about this article.
No comments yet.
Article metadata
About this entry
Event Id
Raw event
Other authors
No one else has published this topic yet.
