Class: Unmagic::Color::OKLCH
- Inherits:
-
Unmagic::Color
- Object
- Unmagic::Color
- Unmagic::Color::OKLCH
- Defined in:
- lib/unmagic/color/oklch.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
Classes: ParseError
Constant Summary
Constants inherited from Unmagic::Color
Instance Attribute Summary collapse
-
#chroma ⇒ Object
readonly
Returns the value of attribute chroma.
-
#hue ⇒ Object
readonly
Returns the value of attribute hue.
Class Method Summary collapse
-
.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:) ⇒ 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.
-
#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_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_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
Constructor Details
#initialize(lightness:, chroma:, hue:) ⇒ OKLCH
Create a new OKLCH color.
93 94 95 96 97 98 |
# File 'lib/unmagic/color/oklch.rb', line 93 def initialize(lightness:, chroma:, hue:) super() @lightness = Color::Lightness.new(lightness * 100) # Convert 0-1 to percentage @chroma = Color::Chroma.new(value: chroma) @hue = Color::Hue.new(value: hue) end |
Instance Attribute Details
#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
.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.
198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/unmagic/color/oklch.rb', line 198 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)”
-
Raw values: “0.65 0.15 240”
-
Space-separated values
132 133 134 135 136 137 138 139 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 |
# File 'lib/unmagic/color/oklch.rb', line 132 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 # 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) end |
Instance Method Details
#==(other) ⇒ Boolean
Check if two OKLCH colors are equal.
410 411 412 413 414 415 |
# File 'lib/unmagic/color/oklch.rb', line 410 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.
343 344 345 346 347 348 349 350 351 352 353 354 |
# File 'lib/unmagic/color/oklch.rb', line 343 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) end |
#darken(amount = 0.03) ⇒ OKLCH
Create a darker version by decreasing lightness.
285 286 287 288 289 |
# File 'lib/unmagic/color/oklch.rb', line 285 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.
312 313 314 315 |
# File 'lib/unmagic/color/oklch.rb', line 312 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.
271 272 273 274 275 |
# File 'lib/unmagic/color/oklch.rb', line 271 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) 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.
106 |
# File 'lib/unmagic/color/oklch.rb', line 106 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.
113 |
# File 'lib/unmagic/color/oklch.rb', line 113 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.
255 256 257 258 |
# File 'lib/unmagic/color/oklch.rb', line 255 def luminance # OKLCH lightness is perceptually uniform, so we can use it directly @lightness.to_ratio # Return 0-1 range end |
#rotate(amount = 10) ⇒ OKLCH
Rotate the hue by a specified amount.
325 326 327 328 |
# File 'lib/unmagic/color/oklch.rb', line 325 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.
299 300 301 302 |
# File 'lib/unmagic/color/oklch.rb', line 299 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_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).
402 403 404 |
# File 'lib/unmagic/color/oklch.rb', line 402 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.
364 365 366 |
# File 'lib/unmagic/color/oklch.rb', line 364 def to_css_oklch format("oklch(%.4f %.4f %.2f)", @lightness.to_ratio, @chroma.value, @hue.value) 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.
379 380 381 |
# File 'lib/unmagic/color/oklch.rb', line 379 def to_css_vars format("--ul:%.4f;--uc:%.4f;--uh:%.2f;", @lightness.to_ratio, @chroma.value, @hue.value) end |
#to_oklch ⇒ OKLCH
Convert to OKLCH color space.
Since this is already an OKLCH color, returns self.
218 219 220 |
# File 'lib/unmagic/color/oklch.rb', line 218 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.
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/unmagic/color/oklch.rb', line 227 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) end |
#to_s ⇒ String
Convert to string representation.
Returns the CSS oklch() function format.
422 423 424 |
# File 'lib/unmagic/color/oklch.rb', line 422 def to_s to_css_oklch end |