-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; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, 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 ).