Class: Unmagic::Color::RGB
- Inherits:
-
Unmagic::Color
- Object
- Unmagic::Color
- Unmagic::Color::RGB
- Defined in:
- lib/unmagic/color/rgb.rb,
lib/unmagic/color/rgb/hex.rb,
lib/unmagic/color/rgb/ansi.rb,
lib/unmagic/color/rgb/named.rb,
lib/unmagic/color/rgb/gradient/linear.rb
Overview
‘RGB` (Red, Green, Blue) color representation.
## Understanding RGB
RGB is how your computer screen creates colors. Every color you see on a screen is made by combining three lights: Red, Green, and Blue. Each light can be set from ‘0` (off) to `255` (full brightness).
Think of it like mixing three flashlights:
-
‘Red=255, Green=0, Blue=0` → Pure red light
-
‘Red=0, Green=255, Blue=0` → Pure green light
-
‘Red=255, Green=255, Blue=0` → Yellow (red + green)
-
‘Red=255, Green=255, Blue=255` → White (all lights on)
-
‘Red=0, Green=0, Blue=0` → Black (all lights off)
## Why 0-255?
Computers store each color component in 8 bits (one byte), which can hold 256 different values (‘0-255`). This gives us `256³ = 16,777,216` possible colors.
## Common Formats
RGB colors can be written in different ways:
-
Hex: ‘#FF5733` (2 hex digits per component: `FF=255, 57=87, 33=51`)
-
Short hex: ‘#F73` (expanded to `#FF7733`)
-
RGB function: ‘rgb(255, 87, 51)`
-
Named colors: ‘goldenrod`, `red`, `blue` (see Named for X11 color names)
## Usage Examples
# Parse from different formats
color = Unmagic::Color::RGB.parse("#FF5733")
color = Unmagic::Color::RGB.parse("rgb(255, 87, 51)")
color = Unmagic::Color::RGB.parse("F73")
# Parse named colors (via RGB::Named or Color.parse)
color = Unmagic::Color::RGB::Named.parse("goldenrod")
color = Unmagic::Color.parse("goldenrod") # Also works
# Create directly
color = Unmagic::Color::RGB.new(red: 255, green: 87, blue: 51)
# Access components
color.red.value #=> 255
color.green.value #=> 87
color.blue.value #=> 51
# Convert to other formats
color.to_hex #=> "#ff5733"
color.to_hsl #=> HSL color
color.to_oklch #=> OKLCH color
# Generate deterministic colors from text
Unmagic::Color::RGB.derive("user@example.com".hash) #=> Consistent color for this string
Defined Under Namespace
Modules: Gradient Classes: ANSI, Hex, Named, ParseError
Constant Summary
Constants inherited from Unmagic::Color
Blue, DATA_PATH, Green, Red, VERSION
Constants included from Harmony
Harmony::SCALE_CHROMA_CURVE, Harmony::SCALE_LIGHTNESS_DEFAULT, Harmony::SCALE_LIGHTNESS_SHAPE
Instance Attribute Summary collapse
-
#alpha ⇒ Object
readonly
Returns the value of attribute alpha.
-
#blue ⇒ Object
readonly
Returns the value of attribute blue.
-
#green ⇒ Object
readonly
Returns the value of attribute green.
-
#red ⇒ Object
readonly
Returns the value of attribute red.
Class Method Summary collapse
-
.build(*args, **kwargs) ⇒ RGB
Build an RGB color from an integer, string, positional values, or keyword arguments.
-
.derive(seed, brightness: 180, saturation: 0.7) ⇒ RGB
Generate a deterministic RGB color from an integer seed.
-
.parse(input) ⇒ RGB
Parse an RGB color from a string.
-
.parse_rgb_format(input) ⇒ RGB
Parse RGB format like “rgb(255, 128, 0)” or “rgb(255 128 0 / 0.5)”.
-
.parse_rgb_values(values) ⇒ Array<Integer>
Parse RGB component values.
Instance Method Summary collapse
-
#==(other) ⇒ Boolean
Check if two RGB colors are equal.
-
#blend(other, amount = 0.5) ⇒ RGB
Blend this color with another color.
-
#darken(amount = 0.1) ⇒ RGB
Create a darker version by blending with black.
-
#initialize(red:, green:, blue:, alpha: nil) ⇒ RGB
constructor
Create a new RGB color.
-
#lighten(amount = 0.1) ⇒ RGB
Create a lighter version by blending with white.
-
#luminance ⇒ Float
Calculate the relative luminance.
-
#pretty_print(pp) ⇒ Object
Pretty print support with colored swatch in class name.
-
#to_ansi(layer: :foreground, mode: :truecolor) ⇒ String
Convert to ANSI SGR color code.
-
#to_hex ⇒ String
Convert to hexadecimal color string.
-
#to_hsl ⇒ HSL
Convert to HSL color space.
-
#to_oklch ⇒ OKLCH
Convert to OKLCH color space.
-
#to_rgb ⇒ RGB
Convert to RGB color space.
-
#to_s ⇒ String
Convert to string representation.
Methods inherited from Unmagic::Color
Methods included from Harmony
#analogous, #complementary, #monochromatic, #scale, #shades, #split_complementary, #tetradic_rectangle, #tetradic_square, #tints, #tones, #triadic
Constructor Details
#initialize(red:, green:, blue:, alpha: nil) ⇒ RGB
Create a new RGB color.
82 83 84 85 86 87 88 |
# File 'lib/unmagic/color/rgb.rb', line 82 def initialize(red:, green:, blue:, alpha: nil) super() @red = Color::Red.new(value: red) @green = Color::Green.new(value: green) @blue = Color::Blue.new(value: blue) @alpha = Color::Alpha.build(alpha) || Color::Alpha::DEFAULT end |
Instance Attribute Details
#alpha ⇒ Object (readonly)
Returns the value of attribute alpha.
65 66 67 |
# File 'lib/unmagic/color/rgb.rb', line 65 def alpha @alpha end |
#blue ⇒ Object (readonly)
Returns the value of attribute blue.
65 66 67 |
# File 'lib/unmagic/color/rgb.rb', line 65 def blue @blue end |
#green ⇒ Object (readonly)
Returns the value of attribute green.
65 66 67 |
# File 'lib/unmagic/color/rgb.rb', line 65 def green @green end |
#red ⇒ Object (readonly)
Returns the value of attribute red.
65 66 67 |
# File 'lib/unmagic/color/rgb.rb', line 65 def red @red end |
Class Method Details
.build(*args, **kwargs) ⇒ RGB
Build an RGB color from an integer, string, positional values, or keyword arguments.
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/unmagic/color/rgb.rb', line 149 def build(*args, **kwargs) # Handle keyword arguments return new(**kwargs) if kwargs.any? # Handle single argument if args.length == 1 value = args[0] # Integer: extract RGB components via bit operations if value.is_a?(::Integer) return new( red: (value >> 16) & 0xFF, green: (value >> 8) & 0xFF, blue: value & 0xFF, ) end # String: delegate to parse return parse(value) if value.is_a?(::String) raise ArgumentError, "Expected Integer or String, got #{value.class}" end # Handle three positional arguments (r, g, b) if args.length == 3 values = args.map { |v| v.is_a?(::String) ? v.to_i : v } return new(red: values[0], green: values[1], blue: values[2]) end raise ArgumentError, "Expected 1 or 3 arguments, got #{args.length}" end |
.derive(seed, brightness: 180, saturation: 0.7) ⇒ RGB
Generate a deterministic RGB color from an integer seed.
This creates consistent, visually distinct colors from hash values or IDs. The same seed always produces the same color, making it useful for:
-
User avatars (hash their email/username)
-
Syntax highlighting (hash the token type)
-
Data visualization (hash category names)
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 |
# File 'lib/unmagic/color/rgb.rb', line 204 def derive(seed, brightness: 180, saturation: 0.7) raise ArgumentError, "Seed must be an integer" unless seed.is_a?(Integer) h32 = seed & 0xFFFFFFFF # Ensure 32-bit # Extract RGB components from different parts of the hash r_base = (h32 & 0xFF) g_base = ((h32 >> 8) & 0xFF) b_base = ((h32 >> 16) & 0xFF) # Apply brightness and saturation adjustments # Brightness controls the average RGB value # Saturation controls how much the channels differ from each other avg = (r_base + g_base + b_base) / 3.0 # Adjust each channel relative to average r = avg + (r_base - avg) * saturation g = avg + (g_base - avg) * saturation b = avg + (b_base - avg) * saturation # Scale to target brightness scale = brightness / 127.5 # 127.5 is middle of 0-255 r = (r * scale).clamp(0, 255).round g = (g * scale).clamp(0, 255).round b = (b * scale).clamp(0, 255).round new(red: r, green: g, blue: b) end |
.parse(input) ⇒ RGB
Parse an RGB color from a string.
Accepts multiple formats:
-
Hex with hash: “#FF8800”, “#F80”
-
Hex without hash: “FF8800”, “F80”
-
RGB function: “rgb(255, 128, 0)”
-
Raw values: “255, 128, 0”
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/unmagic/color/rgb.rb', line 110 def parse(input) raise ParseError, "Input must be a string" unless input.is_a?(::String) input = input.strip # Check for ANSI format first (numeric with optional semicolons) if input.match?(/\A\d+(?:;\d+)*\z/) && ANSI.valid?(input) return ANSI.parse(input) end # Check if it looks like a hex color (starts with # or only contains hex digits) if input.start_with?("#") || input.match?(/\A[0-9A-Fa-f]{3,6}\z/) return Hex.parse(input) end # Try to parse as RGB format parse_rgb_format(input) end |
.parse_rgb_format(input) ⇒ RGB
Parse RGB format like “rgb(255, 128, 0)” or “rgb(255 128 0 / 0.5)”
Supports both legacy comma-separated format and modern space-separated format with optional alpha value.
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 |
# File 'lib/unmagic/color/rgb.rb', line 242 def parse_rgb_format(input) # Remove rgb() or rgba() wrapper if present clean = input.gsub(/^rgba?\s*\(\s*|\s*\)$/, "").strip # Check for modern format with slash (space-separated with / for alpha) # Example: "255 128 0 / 0.5" or "255 128 0 / 50%" if clean.include?("/") parts = clean.split("/").map(&:strip) raise ParseError, "Invalid format with /: expected 'R G B / alpha'" unless parts.length == 2 rgb_values = parts[0].split(/\s+/) alpha_str = parts[1] unless rgb_values.length == 3 raise ParseError, "Expected 3 RGB values before /, got #{rgb_values.length}" end alpha = Color::Alpha.parse(alpha_str) r, g, b = parse_rgb_values(rgb_values) return new(red: r, green: g, blue: b, alpha: alpha) end # Legacy comma-separated format (with or without alpha) # Example: "255, 128, 0" or "255, 128, 0, 0.5" values = clean.split(/\s*,\s*/) unless [3, 4].include?(values.length) raise ParseError, "Expected 3 or 4 RGB values, got #{values.length}" end r, g, b = parse_rgb_values(values[0..2]) alpha = values.length == 4 ? Color::Alpha.parse(values[3]) : nil new(red: r, green: g, blue: b, alpha: alpha) end |
.parse_rgb_values(values) ⇒ Array<Integer>
Parse RGB component values
284 285 286 287 288 289 290 291 292 |
# File 'lib/unmagic/color/rgb.rb', line 284 def parse_rgb_values(values) values.map.with_index do |v, i| unless v.match?(/\A-?\d+\z/) component = ["red", "green", "blue"][i] raise ParseError, "Invalid #{component} value: #{v.inspect} (must be a number)" end v.to_i end end |
Instance Method Details
#==(other) ⇒ Boolean
Check if two RGB colors are equal.
493 494 495 496 497 498 |
# File 'lib/unmagic/color/rgb.rb', line 493 def ==(other) other.is_a?(Unmagic::Color::RGB) && @red == other.red && @green == other.green && @blue == other.blue end |
#blend(other, amount = 0.5) ⇒ RGB
Blend this color with another color.
Blends in RGB space by linearly interpolating each component.
453 454 455 456 457 458 459 460 461 462 463 |
# File 'lib/unmagic/color/rgb.rb', line 453 def blend(other, amount = 0.5) amount = amount.to_f.clamp(0, 1) other_rgb = other.respond_to?(:to_rgb) ? other.to_rgb : other Unmagic::Color::RGB.new( red: (@red.value * (1 - amount) + other_rgb.red.value * amount).round, green: (@green.value * (1 - amount) + other_rgb.green.value * amount).round, blue: (@blue.value * (1 - amount) + other_rgb.blue.value * amount).round, alpha: @alpha.value * (1 - amount) + other_rgb.alpha.value * amount, ) end |
#darken(amount = 0.1) ⇒ RGB
Create a darker version by blending with black.
485 486 487 |
# File 'lib/unmagic/color/rgb.rb', line 485 def darken(amount = 0.1) blend(Unmagic::Color::RGB.new(red: 0, green: 0, blue: 0), amount) end |
#lighten(amount = 0.1) ⇒ RGB
Create a lighter version by blending with white.
473 474 475 |
# File 'lib/unmagic/color/rgb.rb', line 473 def lighten(amount = 0.1) blend(Unmagic::Color::RGB.new(red: 255, green: 255, blue: 255), amount) end |
#luminance ⇒ Float
Calculate the relative luminance.
This is the perceived brightness of the color according to the WCAG specification, accounting for how the human eye responds differently to red, green, and blue light.
424 425 426 427 428 429 430 431 432 433 434 |
# File 'lib/unmagic/color/rgb.rb', line 424 def luminance r = @red.value / 255.0 g = @green.value / 255.0 b = @blue.value / 255.0 r = r <= 0.03928 ? r / 12.92 : ((r + 0.055) / 1.055)**2.4 g = g <= 0.03928 ? g / 12.92 : ((g + 0.055) / 1.055)**2.4 b = b <= 0.03928 ? b / 12.92 : ((b + 0.055) / 1.055)**2.4 0.2126 * r + 0.7152 * g + 0.0722 * b end |
#pretty_print(pp) ⇒ Object
Pretty print support with colored swatch in class name.
Outputs standard Ruby object format with a colored block character embedded in the class name area.
710 711 712 |
# File 'lib/unmagic/color/rgb.rb', line 710 def pretty_print(pp) pp.text("#<#{self.class.name}[\x1b[#{to_ansi(mode: :truecolor)}m█\x1b[0m] @red=#{@red.value} @green=#{@green.value} @blue=#{@blue.value}>") end |
#to_ansi(layer: :foreground, mode: :truecolor) ⇒ String
Convert to ANSI SGR color code.
Returns an ANSI Select Graphic Rendition (SGR) parameter string for terminal output. Supports multiple color modes for different terminal capabilities.
546 547 548 549 550 551 552 553 554 555 556 557 558 |
# File 'lib/unmagic/color/rgb.rb', line 546 def to_ansi(layer: :foreground, mode: :truecolor) raise ArgumentError, "layer must be :foreground or :background" unless [:foreground, :background].include?(layer) raise ArgumentError, "mode must be :truecolor, :palette256, or :palette16" unless [:truecolor, :palette256, :palette16].include?(mode) case mode when :truecolor to_ansi_truecolor(layer) when :palette256 to_ansi_palette256(layer) when :palette16 to_ansi_palette16(layer) end end |
#to_hex ⇒ String
Convert to hexadecimal color string.
Returns a lowercase hex string with hash prefix, always 6 characters (2 per component). If alpha is less than 100%, includes 8 characters with alpha as the last 2 hex digits.
321 322 323 324 325 326 327 328 |
# File 'lib/unmagic/color/rgb.rb', line 321 def to_hex if @alpha.value < 100 alpha_hex = (@alpha.to_ratio * 255).round.to_s(16).rjust(2, "0") format("#%02x%02x%02x%s", @red.value, @green.value, @blue.value, alpha_hex) else format("#%02x%02x%02x", @red.value, @green.value, @blue.value) end end |
#to_hsl ⇒ HSL
Convert to HSL color space.
Converts this RGB color to HSL (Hue, Saturation, Lightness). HSL is often more intuitive for color manipulation.
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 |
# File 'lib/unmagic/color/rgb.rb', line 344 def to_hsl r = @red.value / 255.0 g = @green.value / 255.0 b = @blue.value / 255.0 max = [r, g, b].max min = [r, g, b].min delta = max - min # Lightness l = (max + min) / 2.0 if delta == 0 # Achromatic h = 0 s = 0 else # Saturation s = l > 0.5 ? delta / (2.0 - max - min) : delta / (max + min) # Hue h = case max when r then ((g - b) / delta + (g < b ? 6 : 0)) / 6.0 when g then ((b - r) / delta + 2) / 6.0 when b then ((r - g) / delta + 4) / 6.0 end end Unmagic::Color::HSL.new( hue: (h * 360).round, saturation: (s * 100).round, lightness: (l * 100).round, alpha: @alpha, ) end |
#to_oklch ⇒ OKLCH
Convert to OKLCH color space.
Converts this RGB color to OKLCH (Lightness, Chroma, Hue) using the full OKLab color-science pipeline: gamma-decoded sRGB → linear sRGB →OKLab → OKLCH.
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 |
# File 'lib/unmagic/color/rgb.rb', line 387 def to_oklch require_relative "oklch" r = srgb_to_linear(@red.value / 255.0) g = srgb_to_linear(@green.value / 255.0) b = srgb_to_linear(@blue.value / 255.0) l = Math.cbrt((0.4122214708 * r) + (0.5363325363 * g) + (0.0514459929 * b)) m = Math.cbrt((0.2119034982 * r) + (0.6806995451 * g) + (0.1073969566 * b)) s = Math.cbrt((0.0883024619 * r) + (0.2817188376 * g) + (0.6299787005 * b)) ok_l = (0.2104542553 * l) + (0.7936177850 * m) - (0.0040720468 * s) ok_a = (1.9779984951 * l) - (2.4285922050 * m) + (0.4505937099 * s) ok_b = (0.0259040371 * l) + (0.7827717662 * m) - (0.8086757660 * s) chroma = Math.sqrt((ok_a * ok_a) + (ok_b * ok_b)) hue = Math.atan2(ok_b, ok_a) * 180.0 / Math::PI hue += 360.0 if hue.negative? Unmagic::Color::OKLCH.new(lightness: ok_l, chroma: chroma, hue: hue, alpha: @alpha) end |
#to_rgb ⇒ RGB
Convert to RGB color space.
Since this is already an RGB color, returns self.
300 301 302 |
# File 'lib/unmagic/color/rgb.rb', line 300 def to_rgb self end |
#to_s ⇒ String
Convert to string representation.
Returns the hex representation of the color.
505 506 507 |
# File 'lib/unmagic/color/rgb.rb', line 505 def to_s to_hex end |