Class: Unmagic::Color::OKLCH
- Inherits:
-
Unmagic::Color
- Object
- Unmagic::Color
- Unmagic::Color::OKLCH
- Defined in:
- lib/unmagic/color/oklch.rb,
lib/unmagic/color/oklch/gradient/linear.rb
Overview
‘OKLCH` (Lightness, Chroma, Hue) color representation.
## Understanding OKLCH
OKLCH is a modern color space designed to match how humans actually perceive colors. Unlike RGB or even HSL, OKLCH ensures that colors with the same lightness value look equally bright to our eyes, regardless of their hue.
## The Problem with RGB and HSL
In RGB and HSL, pure yellow and pure blue can have the same “lightness” value, but yellow looks much brighter to our eyes. This makes it hard to create consistent-looking color palettes.
OKLCH solves this by being “perceptually uniform” - if you change lightness by ‘0.1`, it looks like the same amount of change whether you’re working with red, green, blue, or any other hue.
## The Three Components
-
Lightness (‘0.0-1.0`): How bright the color appears
-
‘0.0` = Black
-
‘0.5` = Medium brightness
-
‘1.0` = White
Unlike HSL, this matches perceived brightness consistently across all hues.
-
-
Chroma (‘0.0-0.5`): How colorful/saturated it is
-
‘0.0` = Gray (no color)
-
‘0.15` = Moderate color (good for UI)
-
‘0.3+` = Very vivid (use sparingly)
Think of it like saturation, but more accurate to perception.
-
-
Hue (‘0-360°`): The color itself (same as HSL)
-
‘0°/360°` = Red
-
‘120°` = Green
-
‘240°` = Blue
-
## Why Use OKLCH?
-
Creating accessible color palettes (ensure consistent contrast)
-
Generating color scales that look evenly spaced
-
Interpolating between colors smoothly
-
Matching colors that “feel” equally bright
## When to Use Each Color Space
-
**RGB**: When working with screens/displays directly
-
**HSL**: When you need intuitive color manipulation
-
OKLCH: When you need perceptually accurate colors (design systems, accessibility)
## Examples
# Parse OKLCH colors
color = Unmagic::Color::OKLCH.parse("oklch(0.65 0.15 240)") # Medium blue
# Create directly
accessible = Unmagic::Color::OKLCH.new(lightness: 0.65, chroma: 0.15, hue: 240)
# Access components
color.lightness #=> 0.65 (ratio form)
color.chroma.value #=> 0.15
color.hue.value #=> 240
# Create perceptually uniform variations
lighter = color.lighten(0.05) # Looks 5% brighter
less_colorful = color.desaturate(0.03)
# Generate consistent colors
Unmagic::Color::OKLCH.derive("user@example.com".hash) # Perceptually balanced color
Defined Under Namespace
Modules: Gradient Classes: ParseError
Constant Summary
Constants inherited from Unmagic::Color
Blue, DATA_PATH, Green, Red, VERSION
Instance Attribute Summary collapse
-
#alpha ⇒ Object
readonly
Returns the value of attribute alpha.
-
#chroma ⇒ Object
readonly
Returns the value of attribute chroma.
-
#hue ⇒ Object
readonly
Returns the value of attribute hue.
Class Method Summary collapse
-
.build(*args, **kwargs) ⇒ OKLCH
Build an OKLCH color from a string, positional values, or keyword arguments.
-
.derive(seed, lightness: 0.58, chroma_range: (0.10..0.18), hue_spread: 997, hue_base: 137.508) ⇒ OKLCH
Generate a deterministic OKLCH color from an integer seed.
-
.parse(input) ⇒ OKLCH
Parse an OKLCH color from a string.
Instance Method Summary collapse
-
#==(other) ⇒ Boolean
Check if two OKLCH colors are equal.
-
#blend(other, amount = 0.5) ⇒ OKLCH
Blend this color with another color in OKLCH space.
-
#darken(amount = 0.03) ⇒ OKLCH
Create a darker version by decreasing lightness.
-
#desaturate(amount = 0.02) ⇒ OKLCH
Create a less saturated version by decreasing chroma.
-
#initialize(lightness:, chroma:, hue:, alpha: nil) ⇒ OKLCH
constructor
Create a new OKLCH color.
-
#lighten(amount = 0.03) ⇒ OKLCH
Create a lighter version by increasing lightness.
-
#lightness ⇒ Float
Get the lightness as a ratio (0.0-1.0).
-
#lightness_percentage ⇒ Float
Get the lightness as a percentage (0.0-100.0).
-
#luminance ⇒ Float
Calculate the relative luminance.
-
#pretty_print(pp) ⇒ Object
Pretty print support with colored swatch in class name.
-
#rotate(amount = 10) ⇒ OKLCH
Rotate the hue by a specified amount.
-
#saturate(amount = 0.02) ⇒ OKLCH
Create a more saturated version by increasing chroma.
-
#to_ansi(layer: :foreground, mode: :truecolor) ⇒ String
Convert to ANSI SGR color code.
-
#to_css_color_mix(bg_css = "var(--bg)", a_pct: 72, bg_pct: 28) ⇒ String
Create a CSS color-mix() expression.
-
#to_css_oklch ⇒ String
Convert to CSS oklch() function format.
-
#to_css_vars ⇒ String
Convert to CSS custom properties (variables).
-
#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, #shades, #split_complementary, #tetradic_rectangle, #tetradic_square, #tints, #tones, #triadic
Constructor Details
#initialize(lightness:, chroma:, hue:, alpha: nil) ⇒ OKLCH
Create a new OKLCH color.
97 98 99 100 101 102 103 |
# File 'lib/unmagic/color/oklch.rb', line 97 def initialize(lightness:, chroma:, hue:, alpha: nil) super() @lightness = Color::Lightness.new(value: lightness * 100) # Convert 0-1 to percentage @chroma = Color::Chroma.new(value: chroma) @hue = Color::Hue.new(value: hue) @alpha = Color::Alpha.build(alpha) || Color::Alpha::DEFAULT end |
Instance Attribute Details
#alpha ⇒ Object (readonly)
Returns the value of attribute alpha.
80 81 82 |
# File 'lib/unmagic/color/oklch.rb', line 80 def alpha @alpha end |
#chroma ⇒ Object (readonly)
Returns the value of attribute chroma.
80 81 82 |
# File 'lib/unmagic/color/oklch.rb', line 80 def chroma @chroma end |
#hue ⇒ Object (readonly)
Returns the value of attribute hue.
80 81 82 |
# File 'lib/unmagic/color/oklch.rb', line 80 def hue @hue end |
Class Method Details
.build(*args, **kwargs) ⇒ OKLCH
Build an OKLCH color from a string, positional values, or keyword arguments.
207 208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/unmagic/color/oklch.rb', line 207 def build(*args, **kwargs) if kwargs.any? new(**kwargs) elsif args.length == 1 parse(args[0]) elsif args.length == 3 values = args.map { |v| v.is_a?(::String) ? v.to_f : v } new(lightness: values[0], chroma: values[1], hue: values[2]) else raise ArgumentError, "Expected 1 or 3 arguments, got #{args.length}" end end |
.derive(seed, lightness: 0.58, chroma_range: (0.10..0.18), hue_spread: 997, hue_base: 137.508) ⇒ OKLCH
Generate a deterministic OKLCH color from an integer seed.
Creates perceptually balanced, visually distinct colors. This is particularly effective in OKLCH because the perceptual uniformity ensures all generated colors have consistent perceived brightness and saturation.
The hue distribution uses a golden-angle approach to spread colors evenly and avoid clustering similar hues together.
245 246 247 248 249 250 251 252 253 254 255 256 257 |
# File 'lib/unmagic/color/oklch.rb', line 245 def derive(seed, lightness: 0.58, chroma_range: (0.10..0.18), hue_spread: 997, hue_base: 137.508) raise ArgumentError, "Seed must be an integer" unless seed.is_a?(Integer) h32 = seed & 0xFFFFFFFF # Ensure 32-bit # Hue: golden-angle style distribution to avoid clusters h = (hue_base * (h32 % hue_spread)) % 360 # Chroma: map a byte into a safe text-friendly range c = chroma_range.begin + ((h32 >> 8) & 0xFF) / 255.0 * (chroma_range.end - chroma_range.begin) new(lightness: lightness, chroma: c, hue: h) end |
.parse(input) ⇒ OKLCH
Parse an OKLCH color from a string.
Accepts formats:
-
CSS format: “oklch(0.65 0.15 240)” or “oklch(0.65 0.15 240 / 0.5)”
-
Raw values: “0.65 0.15 240”
-
Space-separated values with optional alpha after slash
140 141 142 143 144 145 146 147 148 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 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/unmagic/color/oklch.rb', line 140 def parse(input) raise ParseError, "Input must be a string" unless input.is_a?(::String) # Remove oklch() wrapper if present clean = input.gsub(/^oklch\s*\(\s*|\s*\)$/, "").strip # Check for alpha with slash separator alpha = nil if clean.include?("/") parts = clean.split("/").map(&:strip) raise ParseError, "Invalid format with /: expected 'L C H / alpha'" unless parts.length == 2 clean = parts[0] alpha = Color::Alpha.parse(parts[1]) end # Split values parts = clean.split(/\s+/) unless parts.length == 3 raise ParseError, "Expected 3 OKLCH values, got #{parts.length}" end # Check if all values are numeric parts.each_with_index do |v, i| unless v.match?(/\A\d+(\.\d+)?\z/) component = ["lightness", "chroma", "hue"][i] raise ParseError, "Invalid #{component} value: #{v.inspect} (must be a number)" end end # Convert to floats l = parts[0].to_f c = parts[1].to_f h = parts[2].to_f # Validate ranges if l < 0 || l > 1 raise ParseError, "Lightness must be between 0 and 1, got #{l}" end if c < 0 || c > 0.5 raise ParseError, "Chroma must be between 0 and 0.5, got #{c}" end if h < 0 || h >= 360 raise ParseError, "Hue must be between 0 and 360, got #{h}" end new(lightness: l, chroma: c, hue: h, alpha: alpha) end |
Instance Method Details
#==(other) ⇒ Boolean
Check if two OKLCH colors are equal.
480 481 482 483 484 485 |
# File 'lib/unmagic/color/oklch.rb', line 480 def ==(other) other.is_a?(Unmagic::Color::OKLCH) && lightness == other.lightness && chroma == other.chroma && hue == other.hue end |
#blend(other, amount = 0.5) ⇒ OKLCH
Blend this color with another color in OKLCH space.
Blending in OKLCH produces perceptually smooth color transitions. Uses shortest-arc hue interpolation to avoid going the long way around the color wheel.
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 |
# File 'lib/unmagic/color/oklch.rb', line 399 def blend(other, amount = 0.5) amount = amount.to_f.clamp(0, 1) other_oklch = other.respond_to?(:to_oklch) ? other.to_oklch : other # Blend in OKLCH space with shortest-arc hue interpolation dh = (((other_oklch.hue.value - @hue.value + 540) % 360) - 180) new_hue = (@hue.value + dh * amount) % 360 new_lightness = lightness + (other_oklch.lightness - lightness) * amount new_chroma = @chroma.value + (other_oklch.chroma.value - @chroma.value) * amount self.class.new( lightness: new_lightness, chroma: new_chroma, hue: new_hue, alpha: @alpha.value * (1 - amount) + other_oklch.alpha.value * amount, ) end |
#darken(amount = 0.03) ⇒ OKLCH
Create a darker version by decreasing lightness.
341 342 343 344 345 |
# File 'lib/unmagic/color/oklch.rb', line 341 def darken(amount = 0.03) current_lightness = @lightness.to_ratio new_lightness = clamp01(current_lightness - amount) self.class.new(lightness: new_lightness, chroma: @chroma.value, hue: @hue.value) end |
#desaturate(amount = 0.02) ⇒ OKLCH
Create a less saturated version by decreasing chroma.
368 369 370 371 |
# File 'lib/unmagic/color/oklch.rb', line 368 def desaturate(amount = 0.02) new_chroma = [@chroma.value - amount, 0.0].max self.class.new(lightness: @lightness.to_ratio, chroma: new_chroma, hue: @hue.value) end |
#lighten(amount = 0.03) ⇒ OKLCH
Create a lighter version by increasing lightness.
In OKLCH, lightness changes are perceptually uniform, so adding 0.05 will look like the same brightness increase regardless of the hue.
327 328 329 330 331 |
# File 'lib/unmagic/color/oklch.rb', line 327 def lighten(amount = 0.03) current_lightness = @lightness.to_ratio new_lightness = clamp01(current_lightness + amount) self.class.new(lightness: new_lightness, chroma: @chroma.value, hue: @hue.value, alpha: @alpha.value) end |
#lightness ⇒ Float
Get the lightness as a ratio (0.0-1.0).
This overrides the attr_reader to return the ratio form, which is the standard way to work with OKLCH lightness.
111 |
# File 'lib/unmagic/color/oklch.rb', line 111 def lightness = @lightness.to_ratio |
#lightness_percentage ⇒ Float
Get the lightness as a percentage (0.0-100.0).
Helper method for when you need the percentage form instead of ratio.
118 |
# File 'lib/unmagic/color/oklch.rb', line 118 def lightness_percentage = @lightness.value |
#luminance ⇒ Float
Calculate the relative luminance.
In OKLCH, the lightness value directly represents perceptual luminance, so we can use it as-is.
311 312 313 314 |
# File 'lib/unmagic/color/oklch.rb', line 311 def luminance # OKLCH lightness is perceptually uniform, so we can use it directly @lightness.to_ratio # Return 0-1 range 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. Note: @lightness is shown via its inspect method since it’s a Lightness percentage object.
525 526 527 |
# File 'lib/unmagic/color/oklch.rb', line 525 def pretty_print(pp) pp.text("#<#{self.class.name}[\x1b[#{to_ansi(mode: :truecolor)}m█\x1b[0m] @lightness=#{@lightness.inspect} @chroma=#{@chroma.value.round(2)} @hue=#{@hue.value.round}>") end |
#rotate(amount = 10) ⇒ OKLCH
Rotate the hue by a specified amount.
381 382 383 384 |
# File 'lib/unmagic/color/oklch.rb', line 381 def rotate(amount = 10) new_hue = (@hue.value + amount) % 360 self.class.new(lightness: @lightness.to_ratio, chroma: @chroma.value, hue: new_hue) end |
#saturate(amount = 0.02) ⇒ OKLCH
Create a more saturated version by increasing chroma.
355 356 357 358 |
# File 'lib/unmagic/color/oklch.rb', line 355 def saturate(amount = 0.02) new_chroma = [@chroma.value + amount, 0.4].min self.class.new(lightness: @lightness.to_ratio, chroma: new_chroma, hue: @hue.value) end |
#to_ansi(layer: :foreground, mode: :truecolor) ⇒ String
Convert to ANSI SGR color code.
Converts to RGB first, then generates the ANSI code.
508 509 510 |
# File 'lib/unmagic/color/oklch.rb', line 508 def to_ansi(layer: :foreground, mode: :truecolor) to_rgb.to_ansi(layer: layer, mode: mode) end |
#to_css_color_mix(bg_css = "var(--bg)", a_pct: 72, bg_pct: 28) ⇒ String
Create a CSS color-mix() expression.
Generates a CSS color-mix expression that blends this color with a background color (typically a CSS variable).
472 473 474 |
# File 'lib/unmagic/color/oklch.rb', line 472 def to_css_color_mix(bg_css = "var(--bg)", a_pct: 72, bg_pct: 28) "color-mix(in oklch, #{to_css_oklch} #{a_pct}%, #{bg_css} #{bg_pct}%)" end |
#to_css_oklch ⇒ String
Convert to CSS oklch() function format.
430 431 432 433 434 435 436 |
# File 'lib/unmagic/color/oklch.rb', line 430 def to_css_oklch if @alpha.value < 100 format("oklch(%.4f %.4f %.2f / %s)", @lightness.to_ratio, @chroma.value, @hue.value, @alpha.to_css) else format("oklch(%.4f %.4f %.2f)", @lightness.to_ratio, @chroma.value, @hue.value) end end |
#to_css_vars ⇒ String
Convert to CSS custom properties (variables).
Outputs the color as CSS variables for lightness, chroma, and hue that can be manipulated or mixed at runtime in CSS.
449 450 451 |
# File 'lib/unmagic/color/oklch.rb', line 449 def to_css_vars format("--ul:%.4f;--uc:%.4f;--uh:%.2f;", @lightness.to_ratio, @chroma.value, @hue.value) end |
#to_hsl ⇒ HSL
Convert to HSL color space.
Converts via RGB as an intermediate step.
274 275 276 |
# File 'lib/unmagic/color/oklch.rb', line 274 def to_hsl to_rgb.to_hsl end |
#to_oklch ⇒ OKLCH
Convert to OKLCH color space.
Since this is already an OKLCH color, returns self.
265 266 267 |
# File 'lib/unmagic/color/oklch.rb', line 265 def to_oklch self end |
#to_rgb ⇒ RGB
This is currently a simplified approximation. A proper OKLCH to sRGB conversion requires more complex color science calculations.
Convert to RGB color space.
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 |
# File 'lib/unmagic/color/oklch.rb', line 283 def to_rgb # For now, convert via approximation - would need proper OKLCH->sRGB conversion # This is a simplified placeholder that approximates RGB from OKLCH require_relative "rgb" # Simple approximation: use lightness and chroma to estimate RGB base = (@lightness.to_ratio * 255).round saturation = (@chroma * 255).value # Convert hue to RGB ratios (very simplified) h_rad = (@hue * Math::PI / 180).value r_offset = (Math.cos(h_rad) * saturation).round g_offset = (Math.cos(h_rad + 2 * Math::PI / 3) * saturation).round b_offset = (Math.cos(h_rad + 4 * Math::PI / 3) * saturation).round r = (base + r_offset).clamp(0, 255) g = (base + g_offset).clamp(0, 255) b = (base + b_offset).clamp(0, 255) Unmagic::Color::RGB.new(red: r, green: g, blue: b, alpha: @alpha) end |
#to_s ⇒ String
Convert to string representation.
Returns the CSS oklch() function format.
492 493 494 |
# File 'lib/unmagic/color/oklch.rb', line 492 def to_s to_css_oklch end |