Packages

Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.

Current section

Files

Jump to
raxol lib raxol style colors gradient.ex
Raw

lib/raxol/style/colors/gradient.ex

alias Raxol.Style.Colors.Color
defmodule Raxol.Style.Colors.Gradient do
@moduledoc """
Creates and manages color gradients for terminal applications.
This module provides functionality for creating gradients between colors and
applying them to text, creating visually striking terminal effects.
## Examples
```elixir
# Create a simple linear gradient between red and blue
alias Raxol.Style.Colors.{Color, Gradient}
red = Color.from_hex("#FF0000")
blue = Color.from_hex("#0000FF")
gradient = Gradient.linear(red, blue, 10)
# Apply the gradient to text
colored_text = Gradient.apply_to_text(gradient, "Hello, World!")
# Create a rainbow gradient
rainbow = Gradient.rainbow(20)
rainbow_text = Gradient.apply_to_text(rainbow, "Rainbow Text")
# Create a multi-stop gradient
colors = [
Color.from_hex("#FF0000"), # Red
Color.from_hex("#FFFF00"), # Yellow
Color.from_hex("#00FF00") # Green
]
multi = Gradient.multi_stop(colors, 15)
```
"""
defstruct [
# List of color stops
:colors,
# Number of discrete steps
:steps,
# Linear, radial, etc.
:type
]
@type gradient_type :: :linear | :rainbow | :heat_map | :multi_stop
@type t :: %__MODULE__{
colors: [Color.t()],
steps: non_neg_integer(),
type: gradient_type()
}
@doc """
Creates a linear gradient between two colors.
## Parameters
- `start_color` - The starting color
- `end_color` - The ending color
- `steps` - The number of color steps in the gradient (including start and end)
## Examples
iex> red = Raxol.Style.Colors.Color.from_hex("#FF0000")
iex> blue = Raxol.Style.Colors.Color.from_hex("#0000FF")
iex> gradient = Raxol.Style.Colors.Gradient.linear(red, blue, 5)
iex> length(gradient.colors)
5
"""
def linear(%Color{} = start_color, %Color{} = end_color, steps)
when is_integer(steps) and steps >= 2 do
colors = generate_gradient_colors(start_color, end_color, steps)
%__MODULE__{
colors: colors,
steps: steps,
type: :linear
}
end
@doc """
Creates a multi-stop gradient with multiple color stops.
## Parameters
- `color_stops` - A list of colors to transition between
- `steps` - The total number of color steps in the gradient
## Examples
iex> colors = [
...> Raxol.Style.Colors.Color.from_hex("#FF0000"),
...> Raxol.Style.Colors.Color.from_hex("#00FF00"),
...> Raxol.Style.Colors.Color.from_hex("#0000FF")
...> ]
iex> gradient = Raxol.Style.Colors.Gradient.multi_stop(colors, 10)
iex> length(gradient.colors)
10
"""
def multi_stop(color_stops, steps)
when is_list(color_stops) and length(color_stops) >= 2 and
is_integer(steps) and steps >= 2 do
# Calculate how many steps to allocate for each segment
segment_count = length(color_stops) - 1
# For even distribution, we need to calculate how many steps per segment
# We add segment_count - 1 to account for the shared points between segments
segments_steps = distribute_steps(steps, segment_count)
# Generate the colors for each segment
colors =
color_stops
|> Enum.chunk_every(2, 1, :discard)
|> Enum.zip(segments_steps)
|> Enum.flat_map(fn {[start_color, end_color], segment_steps} ->
# Generate colors for this segment, excluding the last one (except for the final segment)
segment_colors =
generate_gradient_colors(start_color, end_color, segment_steps)
handle_segment_end(
end_color == List.last(color_stops),
segment_colors
)
end)
%__MODULE__{
colors: colors,
steps: steps,
type: :multi_stop
}
end
@doc """
Creates a rainbow gradient with the given number of steps.
## Parameters
- `steps` - The number of color steps in the rainbow
## Examples
iex> gradient = Raxol.Style.Colors.Gradient.rainbow(6)
iex> length(gradient.colors)
6
"""
def rainbow(steps) when is_integer(steps) and steps >= 2 do
# Create a list of rainbow colors
rainbow_colors = [
# Red
Color.from_hex("#FF0000"),
# Orange
Color.from_hex("#FF7F00"),
# Yellow
Color.from_hex("#FFFF00"),
# Green
Color.from_hex("#00FF00"),
# Blue
Color.from_hex("#0000FF"),
# Indigo
Color.from_hex("#4B0082"),
# Violet
Color.from_hex("#9400D3")
]
# Create a multi-stop gradient with these colors
multi_stop(rainbow_colors, steps)
|> Map.put(:type, :rainbow)
end
@doc """
Creates a heat map gradient from cool to hot colors.
## Parameters
- `steps` - The number of color steps in the heat map
## Examples
iex> gradient = Raxol.Style.Colors.Gradient.heat_map(5)
iex> length(gradient.colors)
5
"""
def heat_map(steps) when is_integer(steps) and steps >= 2 do
# Create a list of heat map colors from cool to hot
heat_colors = [
# Blue (coldest)
Color.from_hex("#0000FF"),
# Cyan
Color.from_hex("#00FFFF"),
# Green
Color.from_hex("#00FF00"),
# Yellow
Color.from_hex("#FFFF00"),
# Red (hottest)
Color.from_hex("#FF0000")
]
# Create a multi-stop gradient with these colors
multi_stop(heat_colors, steps)
|> Map.put(:type, :heat_map)
end
@doc """
Gets the color at a specific position in the gradient.
## Parameters
- `gradient` - The gradient to sample from
- `position` - A value between 0.0 and 1.0 representing the position in the gradient
## Examples
iex> red = Raxol.Style.Colors.Color.from_hex("#FF0000")
iex> blue = Raxol.Style.Colors.Color.from_hex("#0000FF")
iex> gradient = Raxol.Style.Colors.Gradient.linear(red, blue, 5)
iex> color = Raxol.Style.Colors.Gradient.at_position(gradient, 0.5)
iex> color.hex
"#800080" # Purple (mix of red and blue)
"""
def at_position(%__MODULE__{colors: colors}, position)
when is_float(position) and position >= 0.0 and position <= 1.0 do
# Determine the index based on position
index = calculate_position_index(position == 1.0, colors, position)
Enum.at(colors, index)
end
@doc """
Reverses the direction of a gradient.
## Parameters
- `gradient` - The gradient to reverse
## Examples
iex> red = Raxol.Style.Colors.Color.from_hex("#FF0000")
iex> blue = Raxol.Style.Colors.Color.from_hex("#0000FF")
iex> gradient = Raxol.Style.Colors.Gradient.linear(red, blue, 3)
iex> reversed = Raxol.Style.Colors.Gradient.reverse(gradient)
iex> hd(reversed.colors).hex
"#0000FF"
"""
def reverse(%__MODULE__{colors: colors} = gradient) do
%{gradient | colors: Enum.reverse(colors)}
end
@doc """
Applies a gradient to text, returning an ANSI-formatted string.
## Parameters
- `gradient` - The gradient to apply
- `text` - The text to colorize
## Examples
iex> red = Raxol.Style.Colors.Color.from_hex("#FF0000")
iex> blue = Raxol.Style.Colors.Color.from_hex("#0000FF")
iex> gradient = Raxol.Style.Colors.Gradient.linear(red, blue, 5)
iex> Raxol.Style.Colors.Gradient.apply_to_text(gradient, "Hello")
"\e[38;2;255;0;0mH\e[0m\e[38;2;191;0;64me\e[0m\e[38;2;128;0;128ml\e[0m\e[38;2;64;0;191ml\e[0m\e[38;2;0;0;255mo\e[0m"
"""
def apply_to_text(%__MODULE__{colors: colors}, text) when is_binary(text) do
# Split the text into graphemes
graphemes = String.graphemes(text)
# Calculate how to distribute colors across characters
{chars_per_color, remainder} = distribute_colors(colors, graphemes)
# Combine colors with text
colored_chars =
combine_colors_with_text(colors, graphemes, chars_per_color, remainder)
# Join the colored characters
Enum.join(colored_chars)
end
# Also provide to_ansi_sequence as an alias for apply_to_text for API compatibility
@doc """
Alias for apply_to_text/2.
"""
def to_ansi_sequence(gradient, text), do: apply_to_text(gradient, text)
# Private functions
# Generate interpolated colors between start and end
defp generate_gradient_colors(start_color, _end_color, 1), do: [start_color]
defp generate_gradient_colors(start_color, end_color, steps) do
0..(steps - 1)
|> Enum.map(fn step ->
# Calculate the interpolation factor
factor = step / (steps - 1)
# Interpolate between start and end colors
interpolate_color(start_color, end_color, factor)
end)
end
# Interpolate between two colors
defp interpolate_color(
%Color{r: r1, g: g1, b: b1},
%Color{r: r2, g: g2, b: b2},
factor
) do
r = round(r1 + (r2 - r1) * factor)
g = round(g1 + (g2 - g1) * factor)
b = round(b1 + (b2 - b1) * factor)
Color.from_rgb(r, g, b)
end
# Distribute steps across segments
defp distribute_steps(total_steps, segment_count) do
# Calculate the number of intervals to distribute
total_intervals = max(0, total_steps - 1)
# Calculate base intervals per segment
base_intervals = div(total_intervals, segment_count)
# Calculate remaining intervals
remainder_intervals = rem(total_intervals, segment_count)
# Distribute intervals, giving one extra to segments until remainder is used
# Then add 1 to get the number of *colors* needed for each segment's generation
Enum.map(1..segment_count, fn segment_index ->
intervals_for_segment =
calculate_segment_intervals(
segment_index <= remainder_intervals,
base_intervals
)
# Need intervals + 1 colors to cover the intervals
intervals_for_segment + 1
end)
end
# Distribute colors across text
defp distribute_colors(colors, graphemes) do
color_count = length(colors)
char_count = length(graphemes)
handle_color_distribution(
color_count >= char_count,
color_count,
char_count
)
end
# Combine colors with text characters
defp combine_colors_with_text(colors, graphemes, chars_per_color, remainder) do
# If we have more colors than characters, just use the first n colors
handle_color_text_combination(
{chars_per_color == 1, remainder == 0},
colors,
graphemes,
chars_per_color,
remainder
)
end
# Apply a color to a text string
defp colorize_text(text, %Color{r: r, g: g, b: b}) do
# Format as true-color ANSI escape sequence
"\e[38;2;#{r};#{g};#{b}m#{text}\e[0m"
end
# Pattern matching helper functions for refactored if statements
# Handle segment end decision instead of if statement
defp handle_segment_end(true, segment_colors), do: segment_colors
defp handle_segment_end(false, segment_colors),
do: Enum.drop(segment_colors, -1)
# Calculate position index instead of if statement
defp calculate_position_index(true, colors, _position), do: length(colors) - 1
defp calculate_position_index(false, colors, position),
do: trunc(position * length(colors))
# Calculate segment intervals instead of if statement
defp calculate_segment_intervals(true, base_intervals), do: base_intervals + 1
defp calculate_segment_intervals(false, base_intervals), do: base_intervals
# Handle color distribution instead of if statement
defp handle_color_distribution(true, _color_count, _char_count) do
# If we have more or equal colors than characters, we can assign one color per character
{1, 0}
end
defp handle_color_distribution(false, color_count, char_count) do
# Otherwise, calculate how many characters per color
chars_per_color = div(char_count, color_count)
remainder = rem(char_count, color_count)
{chars_per_color, remainder}
end
# Handle color text combination instead of if statement
defp handle_color_text_combination(
{true, true},
colors,
graphemes,
_chars_per_color,
_remainder
) do
Enum.zip(graphemes, colors)
|> Enum.map(fn {char, color} -> colorize_text(char, color) end)
end
defp handle_color_text_combination(
_condition,
colors,
graphemes,
chars_per_color,
remainder
) do
# Distribute colors across characters
colors
|> Enum.with_index()
|> Enum.flat_map(fn {color, index} ->
# Calculate how many characters for this color
extra = calculate_extra_chars(index < remainder)
count = chars_per_color + extra
# Calculate the starting position in the graphemes list
start_pos = index * chars_per_color + min(index, remainder)
# Extract the characters for this color
chars = Enum.slice(graphemes, start_pos, count)
# Apply color to each character
Enum.map(chars, fn char -> colorize_text(char, color) end)
end)
end
# Calculate extra characters instead of if statement
defp calculate_extra_chars(true), do: 1
defp calculate_extra_chars(false), do: 0
end