Packages

A Gleam library for deterministic string to color conversion.

Current section

Files

Jump to
colorhash src colorhash.erl
Raw

src/colorhash.erl

-module(colorhash).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
-define(FILEPATH, "src/colorhash.gleam").
-export([new/0, with_saturations/2, with_lightnesses/2, with_hue_ranges/2, with_hash_fun/2, to_color/2]).
-export_type([color_hash/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" A library for generating deterministic colors from strings.\n"
"\n"
" This library allows you to generate consistent, visually distinct colors\n"
" from any string input. The same string will always produce the same color,\n"
" making it perfect for generating colors for user avatars, data visualization,\n"
" category coloring, or any use case where you need consistent colors without\n"
" manually assigning them.\n"
"\n"
" ## Basic Usage\n"
"\n"
" ```gleam\n"
" import colorhash\n"
" import gleam_community/colour\n"
"\n"
" let hasher = colorhash.new()\n"
" let assert Ok(color) = colorhash.to_color(hasher, \"hello@example.com\")\n"
" \n"
" // Convert to hex for use in HTML/CSS\n"
" let hex = colour.to_rgb_hex_string(color)\n"
" // -> \"53AC76\"\n"
" ```\n"
).
-opaque color_hash() :: {color_hash,
gleam@dict:dict(integer(), float()),
gleam@dict:dict(integer(), float()),
gleam@dict:dict(integer(), {float(), float()}),
fun((binary()) -> integer())}.
-file("src/colorhash.gleam", 33).
-spec sha256(binary()) -> integer().
sha256(S) ->
Hash@1 = case gleam@crypto:hash(sha256, gleam_stdlib:identity(S)) of
<<Hash:32, _/bitstring>> -> Hash;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"colorhash"/utf8>>,
function => <<"sha256"/utf8>>,
line => 34,
value => _assert_fail,
start => 1005,
'end' => 1094,
pattern_start => 1016,
pattern_end => 1035})
end,
Hash@1.
-file("src/colorhash.gleam", 74).
?DOC(
" Creates a new ColorHash with default configuration.\n"
"\n"
" The default configuration provides a good balance of color variety and\n"
" visual appeal for most use cases.\n"
"\n"
" ## Default Values\n"
"\n"
" - **Saturations**: `[0.35, 0.5, 0.65]` - Moderately saturated colors\n"
" - **Lightnesses**: `[0.35, 0.5, 0.65]` - Medium brightness range \n"
" - **Hue ranges**: `[#(0.0, 1.0)]` - Full color spectrum\n"
" - **Hash function**: SHA256 (cryptographically secure, good distribution)\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let hasher = colorhash.new()\n"
" ```\n"
).
-spec new() -> color_hash().
new() ->
{color_hash,
begin
_pipe = [0.35, 0.5, 0.65],
_pipe@1 = gleam@list:index_map(_pipe, fun(X, I) -> {I, X} end),
maps:from_list(_pipe@1)
end,
begin
_pipe@2 = [0.35, 0.5, 0.65],
_pipe@3 = gleam@list:index_map(
_pipe@2,
fun(X@1, I@1) -> {I@1, X@1} end
),
maps:from_list(_pipe@3)
end,
begin
_pipe@4 = [{+0.0, 1.0}],
_pipe@5 = gleam@list:index_map(
_pipe@4,
fun(X@2, I@2) -> {I@2, X@2} end
),
maps:from_list(_pipe@5)
end,
fun sha256/1}.
-file("src/colorhash.gleam", 125).
?DOC(
" Sets the list of possible saturation values for generated colors.\n"
"\n"
" Saturation controls the intensity or purity of a color. \n"
" - `0.0` produces grayscale (no color)\n"
" - `1.0` produces fully saturated (pure) colors\n"
"\n"
" ## Parameters\n"
"\n"
" - `color_hash`: The ColorHash to update\n"
" - `saturations`: List of saturation values, each must be in range `[0.0, 1.0]`\n"
"\n"
" ## Behavior\n"
"\n"
" - The hash value determines which saturation is selected from the list\n"
" - An empty list will cause the default saturations `[0.35, 0.5, 0.65]` to be used\n"
" - Values outside `[0.0, 1.0]` will cause `to_color` to return an error\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" // Grayscale only\n"
" let gray_hasher = \n"
" colorhash.new()\n"
" |> colorhash.with_saturations([0.0])\n"
"\n"
" // Vivid colors only \n"
" let vivid_hasher =\n"
" colorhash.new()\n"
" |> colorhash.with_saturations([0.8, 0.9, 1.0])\n"
"\n"
" // Fixed saturation\n"
" let fixed_hasher =\n"
" colorhash.new()\n"
" |> colorhash.with_saturations([0.7])\n"
" ```\n"
).
-spec with_saturations(color_hash(), list(float())) -> color_hash().
with_saturations(Color_hash, Saturations) ->
_record = Color_hash,
{color_hash,
begin
_pipe = Saturations,
_pipe@1 = gleam@list:index_map(_pipe, fun(X, I) -> {I, X} end),
maps:from_list(_pipe@1)
end,
erlang:element(3, _record),
erlang:element(4, _record),
erlang:element(5, _record)}.
-file("src/colorhash.gleam", 174).
?DOC(
" Sets the list of possible lightness values for generated colors.\n"
"\n"
" Lightness controls the brightness of a color.\n"
" - `0.0` produces black\n"
" - `0.5` produces pure color\n"
" - `1.0` produces white\n"
"\n"
" ## Parameters\n"
"\n"
" - `color_hash`: The ColorHash to update\n"
" - `lightnesses`: List of lightness values, each must be in range `[0.0, 1.0]`\n"
"\n"
" ## Behavior\n"
"\n"
" - The hash value determines which lightness is selected from the list\n"
" - An empty list will cause the default lightnesses `[0.35, 0.5, 0.65]` to be used\n"
" - Values outside `[0.0, 1.0]` will cause `to_color` to return an error\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" // Dark theme colors\n"
" let dark_hasher =\n"
" colorhash.new()\n"
" |> colorhash.with_lightnesses([0.2, 0.3, 0.4])\n"
"\n"
" // Light theme colors\n"
" let light_hasher =\n"
" colorhash.new()\n"
" |> colorhash.with_lightnesses([0.7, 0.8, 0.9])\n"
"\n"
" // High contrast (black or white tendency)\n"
" let contrast_hasher =\n"
" colorhash.new()\n"
" |> colorhash.with_lightnesses([0.1, 0.9])\n"
" ```\n"
).
-spec with_lightnesses(color_hash(), list(float())) -> color_hash().
with_lightnesses(Color_hash, Lightnesses) ->
_record = Color_hash,
{color_hash,
erlang:element(2, _record),
begin
_pipe = Lightnesses,
_pipe@1 = gleam@list:index_map(_pipe, fun(X, I) -> {I, X} end),
maps:from_list(_pipe@1)
end,
erlang:element(4, _record),
erlang:element(5, _record)}.
-file("src/colorhash.gleam", 235).
?DOC(
" Sets the list of hue ranges to constrain generated colors.\n"
"\n"
" Hue determines the base color on the color wheel.\n"
" - `0.0` = Red\n"
" - `0.33` ≈ Green \n"
" - `0.67` ≈ Blue\n"
" - `1.0` = Red (wraps back)\n"
"\n"
" ## Parameters\n"
"\n"
" - `color_hash`: The ColorHash to update\n"
" - `hue_ranges`: List of tuples `#(start, end)` where both values must be in `[0.0, 1.0]`\n"
"\n"
" ## Behavior\n"
"\n"
" - The hash value determines which hue range is selected from the list\n"
" - Within the selected range, the hash determines the exact hue value\n"
" - An empty list will cause the default range `[#(0.0, 1.0)]` to be used\n"
" - Values outside `[0.0, 1.0]` will cause `to_color` to return an error\n"
"\n"
" ## Range Behavior\n"
"\n"
" - **Normal ranges**: When `start < end`, hues are generated between start and end\n"
" - **Inverted ranges**: When `start > end`, the range is treated as swapped (e.g., `#(0.8, 0.2)` generates hues between 0.2 and 0.8)\n"
" - **Zero-width ranges**: When `start == end`, all generated hues will be exactly that value\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" // Warm colors only (reds, oranges, yellows)\n"
" let warm_hasher =\n"
" colorhash.new()\n"
" |> colorhash.with_hue_ranges([#(0.0, 0.17)])\n"
"\n"
" // Cool colors only (blues, greens)\n"
" let cool_hasher =\n"
" colorhash.new()\n"
" |> colorhash.with_hue_ranges([#(0.33, 0.67)])\n"
"\n"
" // Multiple distinct ranges\n"
" let multi_hasher =\n"
" colorhash.new()\n"
" |> colorhash.with_hue_ranges([\n"
" #(0.0, 0.1), // Reds\n"
" #(0.3, 0.4), // Greens\n"
" #(0.6, 0.7), // Blues\n"
" ])\n"
" ```\n"
).
-spec with_hue_ranges(color_hash(), list({float(), float()})) -> color_hash().
with_hue_ranges(Color_hash, Hue_ranges) ->
_record = Color_hash,
{color_hash,
erlang:element(2, _record),
erlang:element(3, _record),
begin
_pipe = Hue_ranges,
_pipe@1 = gleam@list:index_map(_pipe, fun(X, I) -> {I, X} end),
maps:from_list(_pipe@1)
end,
erlang:element(5, _record)}.
-file("src/colorhash.gleam", 288).
?DOC(
" Sets a custom hash function for converting strings to integers.\n"
"\n"
" The hash function determines how input strings are converted to numbers,\n"
" which affects the color distribution and randomness.\n"
"\n"
" ## Parameters\n"
"\n"
" - `color_hash`: The ColorHash to update\n"
" - `hash_fun`: A function that takes a String and returns an Int\n"
"\n"
" ## Behavior\n"
"\n"
" - The default hash function uses SHA256 for cryptographically secure, well-distributed values\n"
" - Custom hash functions can be used for different distributions or performance characteristics\n"
" - Negative hash values are automatically converted to positive using absolute value\n"
" - The same input string should always produce the same hash value for deterministic colors\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" // Use a simple sum of character codes (less random but faster)\n"
" let simple_hasher =\n"
" colorhash.new()\n"
" |> colorhash.with_hash_fun(fn(s) {\n"
" s\n"
" |> string.to_utf_codepoints\n"
" |> list.map(string.utf_codepoint_to_int)\n"
" |> list.fold(0, int.add)\n"
" })\n"
"\n"
" // Use a constant for testing\n"
" let test_hasher =\n"
" colorhash.new()\n"
" |> colorhash.with_hash_fun(fn(_) { 42 })\n"
"\n"
" // Use string length (groups similar length strings) \n"
" let length_hasher =\n"
" colorhash.new()\n"
" |> colorhash.with_hash_fun(string.length)\n"
" ```\n"
).
-spec with_hash_fun(color_hash(), fun((binary()) -> integer())) -> color_hash().
with_hash_fun(Color_hash, Hash_fun) ->
_record = Color_hash,
{color_hash,
erlang:element(2, _record),
erlang:element(3, _record),
erlang:element(4, _record),
Hash_fun}.
-file("src/colorhash.gleam", 338).
?DOC(
" Generates a deterministic color from an input string.\n"
"\n"
" This is the main function of the library. It takes a string input and\n"
" produces a color based on the ColorHash configuration. The same input\n"
" will always produce the same color.\n"
"\n"
" ## Parameters\n"
"\n"
" - `color_hash`: The configured ColorHash containing generation parameters\n"
" - `input`: Any string to convert to a color\n"
"\n"
" ## Returns\n"
"\n"
" - `Ok(Color)`: A color from the `gleam_community/colour` package\n"
" - `Error(Nil)`: If any configured values are outside the valid range `[0.0, 1.0]`\n"
"\n"
" ## Behavior\n"
"\n"
" - The hash value determines which saturation, lightness, and hue range are selected\n"
" - Empty configuration lists fall back to the default values\n"
" - Negative hash values are automatically converted to positive using absolute value\n"
" - Inverted hue ranges (where start > end) are treated as swapped ranges\n"
" - Unicode strings are fully supported\n"
"\n"
" ## Error Cases\n"
"\n"
" Returns `Error(Nil)` when:\n"
" - Any saturation value is outside `[0.0, 1.0]`\n"
" - Any lightness value is outside `[0.0, 1.0]`\n"
" - Any hue range start or end is outside `[0.0, 1.0]`\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
"\n"
" let hasher = colorhash.new()\n"
" let assert Ok(color) = colorhash.to_color(hasher, \"hello@example.com\")\n"
" \n"
" // Convert to hex for use in HTML/CSS\n"
" let hex = colour.to_rgb_hex_string(color)\n"
" // -> \"53AC76\"\n"
" ```\n"
).
-spec to_color(color_hash(), binary()) -> {ok, gleam_community@colour:colour()} |
{error, nil}.
to_color(Color_hash, Input) ->
Default_color_hash = new(),
Hue_ranges = case maps:size(erlang:element(4, Color_hash)) of
0 ->
erlang:element(4, Default_color_hash);
_ ->
erlang:element(4, Color_hash)
end,
Saturations = case maps:size(erlang:element(2, Color_hash)) of
0 ->
erlang:element(2, Default_color_hash);
_ ->
erlang:element(2, Color_hash)
end,
Lightnesses = case maps:size(erlang:element(3, Color_hash)) of
0 ->
erlang:element(3, Default_color_hash);
_ ->
erlang:element(3, Color_hash)
end,
Has_invalid_value = begin
_pipe@4 = [begin
_pipe = Hue_ranges,
_pipe@1 = maps:values(_pipe),
gleam@list:flat_map(
_pipe@1,
fun(X) ->
{Start, End} = X,
[Start, End]
end
)
end,
begin
_pipe@2 = Saturations,
maps:values(_pipe@2)
end,
begin
_pipe@3 = Lightnesses,
maps:values(_pipe@3)
end],
_pipe@5 = lists:append(_pipe@4),
gleam@list:any(_pipe@5, fun(X@1) -> (X@1 < +0.0) orelse (X@1 > 1.0) end)
end,
gleam@bool:guard(
Has_invalid_value,
{error, nil},
fun() ->
Hash = gleam@int:absolute_value(
(erlang:element(5, Color_hash))(Input)
),
Hue_resolution = 727,
gleam@result:'try'(
gleam_stdlib:map_get(Hue_ranges, case maps:size(Hue_ranges) of
0 -> 0;
Gleam@denominator -> Hash rem Gleam@denominator
end),
fun(_use0) ->
{Hue_range_start, Hue_range_end} = _use0,
H = (case erlang:float(Hue_resolution) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@3 -> erlang:float(
case Hue_resolution of
0 -> 0;
Gleam@denominator@2 -> (case maps:size(
Hue_ranges
) of
0 -> 0;
Gleam@denominator@1 -> Hash div Gleam@denominator@1
end)
rem Gleam@denominator@2
end
)
* gleam@float:absolute_value(
Hue_range_end - Hue_range_start
)
/ Gleam@denominator@3
end) + gleam@float:min(Hue_range_start, Hue_range_end),
gleam@result:'try'(
gleam_stdlib:map_get(
Saturations,
case maps:size(Saturations) of
0 -> 0;
Gleam@denominator@4 -> (Hash div 360) rem Gleam@denominator@4
end
),
fun(S) ->
gleam@result:'try'(
gleam_stdlib:map_get(
Lightnesses,
case maps:size(Lightnesses) of
0 -> 0;
Gleam@denominator@6 -> (case maps:size(
Saturations
) of
0 -> 0;
Gleam@denominator@5 -> Hash div 360
div Gleam@denominator@5
end)
rem Gleam@denominator@6
end
),
fun(L) ->
gleam_community@colour:from_hsl(H, S, L)
end
)
end
)
end
)
end
).