A comprehensive Ruby color manipulation library with support for RGB, HSL, and OKLCH color spaces. Parse, convert, and manipulate colors with an intuitive API.
We have a fancy-pants WASM based demo here: https://unreasonable-magic.github.io/unmagic-color/
Add this line to your application's Gemfile:
gem 'unmagic-color'And then execute:
bundle installOr install it yourself as:
gem install unmagic-color# From hex
color = Unmagic::Color.parse("#FF5733")
color = Unmagic::Color["#F57"] # Short form
# From RGB
color = Unmagic::Color.parse("rgb(255, 87, 51)")
color = Unmagic::Color::RGB.new(red: 255, green: 87, blue: 51)
# From HSL
color = Unmagic::Color.parse("hsl(9, 100%, 60%)")
color = Unmagic::Color::HSL.new(hue: 9, saturation: 100, lightness: 60)
# From OKLCH
color = Unmagic::Color.parse("oklch(0.65 0.15 30)")
color = Unmagic::Color::OKLCH.new(lightness: 0.65, chroma: 0.15, hue: 30)
# From named colors (X11 is the default and CSS/W3C databases)
color = Unmagic::Color.parse("goldenrod")
color = Unmagic::Color["red"]
# Pass db name as prefix for specific lookup:
color = Unmagic::Color.parse("css:red")
color = Unmagic::Color.parse("w3c:gray") # w3c is alias for css
color = Unmagic::Color.parse("x11:red")
# Named colors are case-insensitive and whitespace-tolerant
color = Unmagic::Color.parse("Golden Rod") # Same as "goldenrod"
color = Unmagic::Color.parse("DARK SLATE BLUE") # Same as "darkslateblue"Named Color Databases:
- X11: 658 colors, ~54 KB in memory (lazy-loaded)
- CSS/W3C: 148 colors, ~13 KB in memory (lazy-loaded)
Databases are only loaded into memory when first accessed, with sub-millisecond load times.
# From ANSI escape codes
color = Unmagic::Color.parse("31") # Red (standard ANSI)
color = Unmagic::Color.parse("38;5;196") # Red (256-color palette)
color = Unmagic::Color.parse("38;2;255;0;0") # Red (24-bit true color)Generate ANSI escape codes for colorful terminal output:
# Convert any color to ANSI
color = Unmagic::Color.parse("goldenrod")
puts "\x1b[#{color.to_ansi}mHello World!\x1b[0m"
# Foreground colors (default)
red = Unmagic::Color.parse("#ff0000")
puts "\x1b[#{red.to_ansi}mRed text\x1b[0m"
# Background colors
blue = Unmagic::Color.parse("#0000ff")
puts "\x1b[#{blue.to_ansi(layer: :background)}mBlue background\x1b[0m"
# Default mode is 24-bit true color
Unmagic::Color.parse("red").to_ansi # => "38;2;255;0;0"
Unmagic::Color.parse("css:green").to_ansi # => "38;2;0;128;0"
# Named colors can use standard codes with palette16 mode
Unmagic::Color.parse("red").to_ansi(mode: :palette16) # => "91"
Unmagic::Color.parse("green").to_ansi(mode: :palette16) # => "92"
# Custom colors also use 24-bit true color by default
Unmagic::Color.parse("#6496c8").to_ansi # => "38;2;100;150;200"
# Parse ANSI codes back to colors
color = Unmagic::Color.parse("38;2;100;150;200")
color.to_hex # => "#6496c8"Supported ANSI formats:
- 3/4-bit colors:
30-37(foreground),40-47(background),90-97(bright foreground),100-107(bright background) - 256-color palette:
38;5;N(foreground),48;5;N(background) where N is 0-255 - 24-bit true color:
38;2;R;G;B(foreground),48;2;R;G;B(background)
rgb = Unmagic::Color.parse("#FF5733")
# Convert to HSL
hsl = rgb.to_hsl
puts hsl.hue.to_f # => 11.0
puts hsl.saturation.to_f # => 100.0
puts hsl.lightness.to_f # => 60.0
# Convert to OKLCH
oklch = rgb.to_oklch
# Convert back to hex
hex = hsl.to_rgb.to_hex
puts hex # => "#ff5833"color = Unmagic::Color.parse("#336699")
# Make it lighter or darker
lighter = color.lighten(0.2) # 20% lighter
darker = color.darken(0.1) # 10% darker
# Blend two colors
red = Unmagic::Color.parse("#FF0000")
blue = Unmagic::Color.parse("#0000FF")
purple = red.blend(blue, 0.5) # 50/50 mix
# Check if color is light or dark
if color.light?
text_color = "#000000" # Use dark text
else
text_color = "#FFFFFF" # Use light text
endGenerate color palettes based on color theory relationships:
color = Unmagic::Color.parse("#FF5733")
# Complementary - opposite on the color wheel (180°)
color.complementary.to_hex # => "#33daff"
# Analogous - adjacent colors (default ±30°)
color.analogous.map(&:to_hex) # => ["#ff3374", "#ffbe33"]
# Triadic - three colors equally spaced (120° apart)
color.triadic.map(&:to_hex) # => ["#33ff58", "#5833ff"]
# Split Complementary - complement's neighbors (default 180° ±30°)
color.split_complementary.map(&:to_hex) # => ["#33ffbe", "#3374ff"]
# Tetradic Square - four colors equally spaced (90° apart)
color.tetradic_square.map(&:to_hex) # => ["#74ff33", "#33daff", "#be33ff"]
# Tetradic Rectangle - two complementary pairs (default 60°)
color.tetradic_rectangle.map(&:to_hex) # => ["#daff33", "#33daff", "#5833ff"]Generate shades, tints, tones, and monochromatic palettes:
color = Unmagic::Color.parse("#3366CC")
# Monochromatic - same hue with varying lightness
color.monochromatic(steps: 5).map(&:to_hex) # => ["#0f1f3d", "#214285", "#3366cc", "#7a9cde", "#c2d1f0"]
# Shades - progressively darker (mixed with black)
shades = color.shades(steps: 5)
shades = color.shades(steps: 5, amount: 0.8) # Darker range
# Tints - progressively lighter (mixed with white)
tints = color.tints(steps: 5)
tints = color.tints(steps: 5, amount: 0.8) # Lighter range
# Tones - progressively desaturated (mixed with gray)
tones = color.tones(steps: 5)
tones = color.tones(steps: 5, amount: 0.8) # More muted rangeAll harmony and variation methods preserve the original color space:
hsl = Unmagic::Color::HSL.new(hue: 200, saturation: 80, lightness: 50)
hsl.complementary.to_hex # => "#e65e19"
hsl.shades(steps: 3).map(&:to_hex) # => ["#1587bf", "#116c99", "#0d5173"]
rgb = Unmagic::Color.parse("#FF5733")
rgb.analogous.map(&:to_hex) # => ["#ff3374", "#ffbe33"]hsl = Unmagic::Color::HSL.new(hue: 200, saturation: 70, lightness: 50)
# Adjust individual components
brighter = hsl.lighten(0.15)
muted = hsl.desaturate(0.3)
shifted = hsl.adjust_hue(30)
# Create color progressions
hsl.progression(steps: 5, lightness: [30, 50, 70, 85, 95]).map(&:to_hex)
# => ["#175e82", "#269dd9", "#7dc4e8", "#bee2f4", "#e9f5fb"]Generate consistent, deterministic colors from any string:
# Generate from a user ID or name
color = Unmagic::Color::RGB.derive("user_12345".hash)
color = Unmagic::Color::HSL.derive("[email protected]".hash)
# Customize the output
color = Unmagic::Color::RGB.derive(
"project_alpha".hash,
brightness: 200, # Brighter colors
saturation: 0.8 # More saturated
)
color = Unmagic::Color::HSL.derive(
"team_rocket".hash,
lightness: 60, # Target lightness
saturation_range: (50..70) # Saturation range
)For more control over how strings map to colors, use hash functions:
# Use different hash algorithms
hash_value = Unmagic::Color::String::HashFunction::DJB2.call("username")
color = Unmagic::Color::RGB.derive(hash_value)
# Color-aware hashing (biases toward color names in string)
hash_value = Unmagic::Color::String::HashFunction::COLOR_AWARE.call("red_team")
# Will produce a reddish color
# Available hash functions:
# - SUM: Simple, anagrams get same color
# - DJB2: Good general-purpose distribution
# - BKDR: Excellent distribution (default)
# - FNV1A: Maximum variety for sequential strings
# - SDBM: Good for database IDs
# - JAVA: Compatible with Java's hashCode
# - CRC32: Most uniform distribution
# - MD5: Cryptographic hash (slower)
# - POSITION: Order-sensitive
# - PERCEPTUAL: Case-insensitive
# - COLOR_AWARE: Detects color names
# - MURMUR3: Fast with excellent distributionrgb = Unmagic::Color::RGB.new(red: 255, green: 87, blue: 51)
# Access components
puts rgb.red.to_i # => 255
puts rgb.green.to_i # => 87
puts rgb.blue.to_i # => 51
hsl = Unmagic::Color::HSL.new(hue: 180, saturation: 50, lightness: 60)
# Access components
puts hsl.hue.to_f # => 180.0
puts hsl.saturation.to_f # => 50.0
puts hsl.lightness.to_f # => 60.0
# Components support arithmetic
new_hue = hsl.hue + 30 # Shift hue by 30 degreescolor = Unmagic::Color.parse("#336699")
# Get luminance (0.0 = black, 1.0 = white)
lum = color.luminance # => ~0.2
# Check if light or dark
color.light? # => false
color.dark? # => true
# Choose contrasting text color
text_color = color.light? ? "#000000" : "#FFFFFF"Create smooth color transitions with gradients in RGB, HSL, or OKLCH color spaces:
# Simple gradient - auto-detects color space
gradient = Unmagic::Color::Gradient.linear(["#FF0000", "#0000FF"])
bitmap = gradient.rasterize(width: 10)
# Access the colors
colors = bitmap.pixels[0] # Array of 10 colors from red to blue
colors.map(&:to_hex)
# => ["#ff0000", "#e60019", "#cc0033", ..., "#0000ff"]
# Create gradients with multiple stops
gradient = Unmagic::Color::Gradient.linear([
"#FF0000", # Red at start (0%)
"#00FF00", # Green at middle (50%)
"#0000FF" # Blue at end (100%)
])
bitmap = gradient.rasterize(width: 20)
# Use explicit positions (like CSS linear-gradient)
gradient = Unmagic::Color::Gradient.linear([
["#FF0000", 0.0], # Red at start
["#FFFF00", 0.3], # Yellow at 30%
"#00FF00", # Green auto-balances at 65%
["#0000FF", 1.0] # Blue at end
])
# Specify gradient direction
gradient = Unmagic::Color::Gradient.linear(
["#FF0000", "#0000FF"],
direction: "to right"
)
# Use angle directions
gradient = Unmagic::Color::Gradient.linear(
["#FF0000", "#0000FF"],
direction: "45deg" # or just 45
)
# 2D gradients with width and height
gradient = Unmagic::Color::Gradient.linear(
["#FF0000", "#0000FF"],
direction: "to bottom right"
)
bitmap = gradient.rasterize(width: 100, height: 100)
# Access individual pixels
color = bitmap.at(50, 50) # Get color at x=50, y=50
# Choose color space for different effects
# RGB gradients - direct color component interpolation
rgb_gradient = Unmagic::Color::RGB::Gradient::Linear.build(["#FF0000", "#0000FF"])
# HSL gradients - smoother transitions through the color wheel
hsl_gradient = Unmagic::Color::HSL::Gradient::Linear.build([
"hsl(0, 100%, 50%)", # Red
"hsl(240, 100%, 50%)" # Blue
])
# OKLCH gradients - perceptually uniform transitions
oklch_gradient = Unmagic::Color::OKLCH::Gradient::Linear.build([
"oklch(0.5 0.15 30)", # Orange
"oklch(0.7 0.15 240)" # Blue
])Supported direction formats:
- Keywords:
"to top","to right","to bottom left", etc. - From/to:
"from left to right","from bottom to top" - Angles:
"45deg","90deg", or numeric values like45,90 - Direction objects:
Unmagic::Color::Units::Degrees::Direction::LEFT_TO_RIGHT
Standard color space for displays. Values range from 0-255 for each component.
Unmagic::Color::RGB.new(red: 255, green: 87, blue: 51)Intuitive color space for human-friendly color manipulation.
- Hue: 0-360 degrees (color wheel position)
- Saturation: 0-100% (color intensity)
- Lightness: 0-100% (brightness)
Unmagic::Color::HSL.new(hue: 180, saturation: 70, lightness: 50)Perceptually uniform color space that better matches human color perception.
Unmagic::Color::OKLCH.new(lightness: 0.65, chroma: 0.15, hue: 180)- Ruby >= 3.0
After checking out the repo, run bundle install to install dependencies. Then, run bundle exec rspec to run the tests.
Bug reports and pull requests are welcome on GitHub at https://github.com/unreasonable-magic/unmagic-color.
The gem is available as open source under the terms of the MIT License.

