Packages

Colorimetry and theme/palette management library for Elixir - parse, convert, harmonize, and render colors across multiple color spaces.

Current section

Files

Jump to
pote README.md
Raw

README.md

<p align="center">
<img src="https://raw.githubusercontent.com/Lorenzo-SF/pote/main/docs/batamantaman_pote.png" width="400" alt="Pote Mascot" />
</p>
# Pote — Colorimetry and theme management for Elixir
[![Hex Version](https://img.shields.io/hexpm/v/pote.svg)](https://hex.pm/packages/pote)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3.svg)](https://hexdocs.pm/pote)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.md)
[![Version](https://img.shields.io/badge/version-2.0.0-blue.svg)](https://github.com/Lorenzo-SF/pote)
Pote is an Elixir library for comprehensive color manipulation: parsing, conversion
between all major color spaces, harmony generation, gradient creation, accessibility
checks, and terminal ANSI output.
## Quick Start
Add `pote` to your `mix.exs`:
```elixir
def deps do
[
{:pote, github: "Lorenzo-SF/pote"}
]
end
```
Basic usage:
```elixir
# Parse a color from any format into RGB
{:ok, rgb} = Pote.Orchestrator.parse_color("#FF8000")
{:ok, rgb} = Pote.Orchestrator.parse_color("hsl:30,100,50")
{:ok, rgb} = Pote.Orchestrator.parse_color(:red)
# Generate harmonies
Pote.Harmonies.complementary({255, 87, 51})
# => [{51, 219, 255}]
Pote.Harmonies.triad({255, 87, 51})
# => [{51, 255, 87}, {87, 51, 255}]
# Create gradients
Pote.Gradients.linear({255, 0, 0}, {0, 0, 255}, 5)
# => [{255, 0, 0}, {191, 0, 64}, {128, 0, 128}, {64, 0, 191}, {0, 0, 255}]
# Apply gradient to text for terminal output
Pote.Gradients.apply_to_text("Hello, world!", {255, 0, 0}, {0, 0, 255})
```
## Features
- **Color parsing** — Accept RGB, HEX, HSL, HSV, CMYK, HWB, XTerm256, named colors,
ARGB, and theme colors from strings, tuples, or atoms.
- **Conversion** — Bidirectional conversion between RGB, HEX, HSL, HSV, CMYK,
XTerm256, CIE XYZ, CIELAB, YUV, YCbCr, HWB, and Kelvin.
- **Harmonies** — Complementary, analogous, triad, square,
split-complementary, compound, and monochromatic color schemes.
- **Gradients** — Linear, multi-stop gradients; apply foreground/background
gradients to text for terminal UIs; vertical gradient fills.
- **ANSI output** — Generate true-color and 256-color ANSI escape sequences
for foreground and background.
- **Accessibility** — WCAG 2.1 relative luminance, contrast ratio, and
Delta E 1976 color distance.
- **Validation** — Validate color format strings with descriptive error messages.
- **Pantone approximation** — Find the closest Pantone match for any RGB color.
- **Named colors** — Built-in palette of basic, bright, light, and theme colors
with custom theme support.
- **ColorInfo struct** — Convenient struct for storing a color in all formats
with harmony helpers.
## Supported Color Formats
| Format | Input Examples | Range |
|------------|-----------------------------------------|-------------------------------------------|
| RGB | `{255, 128, 0}`, `"rgb:255,128,0"` | 0–255 per channel |
| ARGB | `"argb:255,255,128,0"` | 0–255 per channel (alpha ignored) |
| HEX | `"#FF8000"`, `"FF8000"`, `"#F80"` | `#RRGGBB`, `#RGB` |
| HSL | `{30.0, 100.0, 50.0}`, `"hsl:30,100,50"` | H: 0–360°, S/L: 0–100% |
| HSV | `{30.0, 100.0, 100.0}`, `"hsv:30,100,100"` | H: 0–360°, S/V: 0–100% |
| CMYK | `"cmyk:0,50,100,0"` | 0–100% per channel |
| HWB | `"hwb:30,0.2,0.3"` | H: 0–360°, W/B: 0.0–1.0 |
| XTerm256 | `208`, `"xterm:208"` | 0–255 |
| Named | `:red`, `"cyan"`, `"bright_green"` ||
| Theme | `"theme:primary"`, `"theme:error"` ||
| XYZ | — (conversion output) ||
| CIELAB | — (conversion output) | L: 0–100, a/b: ~–128–127 |
| YUV | — (conversion output) | Y: 0–255, U/V: –128–127 |
| YCbCr | — (conversion output) | Y: 16–235, Cb/Cr: 16–240 |
| Kelvin | `Pote.Converters.Advanced.kelvin_to_rgb(6500)` | 1000–40000 |
## Usage Examples
### Parse any color into RGB
```elixir
alias Pote.Orchestrator
Orchestrator.parse_color("#FF8000")
# => {:ok, {255, 128, 0}}
Orchestrator.parse_color("rgb:255,128,0")
# => {:ok, {255, 128, 0}}
Orchestrator.parse_color("hsl:30,100,50")
# => {:ok, {255, 128, 0}}
Orchestrator.parse_color(:magenta)
# => {:ok, {255, 0, 255}}
Orchestrator.parse_color("theme:primary")
# => {:ok, {161, 231, 250}}
# Bang (!) variant
Orchestrator.to_rgb!("#FF8000")
# => {255, 128, 0}
```
### Convert between color spaces
```elixir
alias Pote.Converters
alias Pote.Converters.Advanced
Converters.rgb_to_hex({255, 128, 0})
# => "#FF8000"
Converters.rgb_to_hsl({255, 128, 0})
# => {30.0, 100.0, 50.0}
Converters.rgb_to_cmyk({255, 128, 0})
# => {0.0, 49.8, 100.0, 0.0}
Converters.rgb_to_xterm256({255, 128, 0})
# => 208
Converters.hsl_to_rgb({30.0, 100.0, 50.0})
# => {255, 128, 0}
# Advanced: color temperature
Advanced.kelvin_to_rgb(6500)
# => {255, 249, 253}
Advanced.rgb_to_kelvin({255, 160, 60})
# => 3200
# Advanced: video color spaces
Advanced.to_yuv({255, 128, 0})
# => {165, 13, 146}
Advanced.to_ycbcr({255, 128, 0})
# => {165, 69, 224}
```
### Terminal ANSI output
```elixir
alias Pote.Orchestrator
# Foreground ANSI escape code
Orchestrator.to_ansi({255, 128, 0})
# => "\e[38;2;255;128;0m"
Orchestrator.to_ansi("#FF8000")
# => "\e[38;2;255;128;0m"
# Background ANSI escape code
Orchestrator.to_ansi_bg({255, 128, 0})
# => "\e[48;2;255;128;0m"
# Convert to XTerm256 index
Orchestrator.to_xterm256({255, 128, 0})
# => {:ok, 208}
# Print colored text in terminal
IO.puts("#{Orchestrator.to_ansi({255, 128, 0})}Hello in orange!#{IO.ANSI.reset()}")
```
### Color harmonies
```elixir
alias Pote.Harmonies
color = {255, 87, 51}
Harmonies.complementary(color)
# => [{51, 219, 255}]
Harmonies.analogous(color)
# => [{255, 128, 0}, {255, 0, 128}]
Harmonies.triad(color)
# => [{51, 255, 87}, {87, 51, 255}]
Harmonies.square(color)
# => [{179, 255, 51}, {51, 219, 255}, {87, 51, 255}]
Harmonies.monochromatic(color, 5)
# => [darker...to...lighter variations]
Harmonies.split_complementary(color)
# => [{87, 255, 51}, {219, 51, 255}]
# Lightness utilities
Harmonies.lighter(color, 0.2)
# => blends with white
Harmonies.darker(color, 0.4)
# => blends with black
```
### Gradients
```elixir
alias Pote.Gradients
# Linear gradient between two colors
Gradients.linear({255, 0, 0}, {0, 0, 255}, 5)
# => [{255, 0, 0}, {191, 0, 64}, {128, 0, 128}, {64, 0, 191}, {0, 0, 255}]
# Multi-stop gradient
Gradients.multicolor([{255, 0, 0}, {0, 255, 0}, {0, 0, 255}], 5)
# => [{255, 0, 0}, {128, 128, 0}, {0, 255, 0}, {0, 128, 128}, {0, 0, 255}]
# Gradient text (terminal)
Gradients.apply_to_text("Pote", {255, 0, 0}, {0, 0, 255})
# => iodata with gradient-colored characters
# Gradient background for text
Gradients.apply_bg_to_text("Pote", {255, 0, 0}, {0, 0, 255})
# Vertical gradient fill
Gradients.vertical_fill({0, 0, 100}, {100, 0, 0}, 5, 10)
```
### Accessibility
```elixir
alias Pote.Converters.Advanced
# WCAG 2.1 contrast ratio
Advanced.contrast_ratio({255, 255, 255}, {0, 0, 0})
# => 21.0
# WCAG 2.1 relative luminance
Advanced.relative_luminance({0, 128, 0})
# => 0.25016
# Delta E 1976 color distance (< 1.0 is imperceptible)
Advanced.delta_e({255, 0, 0}, {254, 0, 0})
# => ~0.4
```
### Validation
```elixir
alias Pote.Validator
Validator.validate("hex:FF0000")
# => :ok
Validator.validate("rgb:256,0,0")
# => {:error, :rgb_value_out_of_range}
Validator.error_message(:rgb_value_out_of_range)
# => "RGB values must be integers between 0 and 255"
```
### ColorInfo struct
```elixir
alias Pote.ColorInfo
# Create from any color input
ci = ColorInfo.new({255, 128, 0})
%ColorInfo{rgb: {255, 128, 0}, hex: "#FF8000", hsl: {30.0, 100.0, 50.0}, ...}
# ANSI escape
ColorInfo.to_ansi(ci)
# Harmony methods on the struct
ColorInfo.complementary(ci)
ColorInfo.triad(ci)
ColorInfo.analogous(ci, 15.0)
ColorInfo.lighter(ci, 0.3)
ColorInfo.darker(ci, 0.3)
```
### Default palette
```elixir
Pote.default_colors()
# => %{primary: {161, 231, 250}, secondary: {58, 171, 163}, ...}
Pote.get_color(:primary)
# => {161, 231, 250}
Pote.color_names()
# => [:primary, :secondary, :ternary, ...]
```
## Theming with `use Pote.Theme`
Host applications can opt into a complete theme system by calling
`use Pote.Theme`. The macro generates a facade module that owns its
own themes on disk, integrates with Pote's resolver stack, and exposes
the full theme management API.
### Quick start
```elixir
defmodule MyApp.Theme do
use Pote.Theme,
config_app: :my_app,
storage_dir: "~/.config/my_app/themes",
defaults: %{
"primary" => {0, 120, 215},
"accent" => {255, 90, 50},
"background" => {24, 24, 28},
"text" => {240, 240, 245}
}
end
```
That's it — `MyApp.Theme` now exposes:
```elixir
MyApp.Theme.list() # => ["default", "dracula", "monokai", ...]
MyApp.Theme.active() # => %Pote.Theme{...}
MyApp.Theme.activate("dracula")
MyApp.Theme.color("primary") # => {0, 120, 215}
MyApp.Theme.colors() # => %{"primary" => {0, 120, 215}, ...}
MyApp.Theme.install!(MyApp.Theme.templates().dracula)
```
### Built-in templates
Five palettes ship in the box:
```elixir
MyApp.Theme.templates()
# => ["default", "dracula", "monokai", "nord", "light"]
MyApp.Theme.install_template("dracula")
# writes ~/.config/my_app/themes/dracula.json
```
### Wiring up to `Pote.parse/1`
For `"theme:<key>"` lookups to consult the active theme, the host must
register the resolver with Pote. Calling `MyApp.Theme.register_with_pote/0`
(or `MyApp.Theme.ensure_registered/0`, called lazily by every other
function) pushes the resolver onto Pote's stack:
```elixir
# In your Application.start/2
:ok = MyApp.Theme.register_with_pote()
```
From that point on, `Pote.parse("theme:primary")` reads from the active
theme instead of Pote's hardcoded `@default_colors` palette.
### Customising `storage_dir/0`
Host modules can override `storage_dir/0` to read from a runtime source
(for example, an environment variable). The macro exposes the opt value
as the `@storage_dir_default` attribute:
```elixir
defmodule MyApp.Theme do
use Pote.Theme,
config_app: :my_app,
storage_dir: "~/.config/my_app/themes",
defaults: %{}
def storage_dir, do: System.get_env("MYAPP_THEMES_PATH") || @storage_dir_default
end
```
`storage_dir/0` is consulted on every theme lookup, so changes to the
underlying directory are picked up without re-registering the resolver.
### Multiple consumers
Pote's resolver is a **stack**, not a single function. Several apps can
each register their own `use Pote.Theme` resolver at boot without
overwriting each other. Each lookup walks the stack and returns the
first non-`:not_found` hit. `Pote.put_theme_resolver(:pop)` removes the
most recent resolver; `Pote.put_theme_resolver(nil)` clears the stack.
Add `pote` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:pote, "~> 1.0.0"}
]
end
```
Generate documentation with ExDoc:
```sh
mix docs
```
## Project history
This library was developed as part of a larger internal toolkit and extracted
to open source in mid-2026. The single commit visible on `main` represents the
OSS cut-over point — all the features shipped in `1.0.0` were built and tested
before being made public. Subsequent releases (`1.0.1`, `1.1.0`, ...) will be
tagged normally, providing a clean public history going forward.
A Spanish version of this README is available at [`README_ES.md`](./README_ES.md).
## License
MIT License. See [LICENSE](LICENSE) for details.