Class: Unmagic::Color::HSL
- Inherits:
-
Unmagic::Color
- Object
- Unmagic::Color
- Unmagic::Color::HSL
- Defined in:
- lib/unmagic/color/hsl.rb
Overview
‘HSL` (Hue, Saturation, Lightness) color representation.
## Understanding HSL
While RGB describes colors as mixing light, HSL describes colors in a way that’s more intuitive to humans. It separates the “what color” from “how vibrant” and “how bright.”
## The Three Components
-
Hue (‘0-360°`): The actual color on the color wheel
-
‘0°/360°` = Red
-
‘60°` = Yellow
-
‘120°` = Green
-
‘180°` = Cyan
-
‘240°` = Blue
-
‘300°` = Magenta
Think of it as rotating around a circle of colors.
-
-
Saturation (‘0-100%`): How pure/intense the color is
-
‘0%` = Gray (no color, just brightness)
-
‘50%` = Moderate color
-
‘100%` = Full, vivid color
Think of it as “how much color” vs “how much gray.”
-
-
Lightness (‘0-100%`): How bright the color is
-
‘0%` = Black (no light)
-
‘50%` = Pure color
-
‘100%` = White (full light)
Think of it as a dimmer switch.
-
## Why HSL is Useful
HSL makes it easy to:
-
Create color variations (keep hue, adjust saturation/lightness)
-
Generate color schemes (change hue by fixed amounts)
-
Make colors lighter/darker without changing their “color-ness”
## Common Patterns
-
**Pastel colors**: High lightness, medium-low saturation (‘70-80% L`, `30-50% S`)
-
**Vibrant colors**: Medium lightness, high saturation (‘50% L`, `80-100% S`)
-
**Dark colors**: Low lightness, any saturation (‘20-30% L`)
-
**Muted colors**: Medium lightness and saturation (‘40-60% L`, `30-50% S`)
## Examples
# Parse HSL colors
color = Unmagic::Color::HSL.parse("hsl(120, 100%, 50%)") # Pure green
color = Unmagic::Color::HSL.parse("240, 50%, 75%") # Light blue
# Create directly
red = Unmagic::Color::HSL.new(hue: 0, saturation: 100, lightness: 50)
pastel = Unmagic::Color::HSL.new(hue: 180, saturation: 40, lightness: 80)
# Access components
color.hue.value #=> 120 (degrees)
color.saturation.value #=> 100 (percent)
color.lightness.value #=> 50 (percent)
# Easy color variations
lighter = color.lighten(0.2) # Increase lightness
muted = color.desaturate(0.3) # Reduce saturation
# Generate color from text
Unmagic::Color::HSL.derive("user@example.com".hash) # Consistent color
Defined Under Namespace
Classes: ParseError
Constant Summary
Constants inherited from Unmagic::Color
Instance Attribute Summary collapse
-
#hue ⇒ Object
readonly
Returns the value of attribute hue.
-
#lightness ⇒ Object
readonly
Returns the value of attribute lightness.
-
#saturation ⇒ Object
readonly
Returns the value of attribute saturation.
Class Method Summary collapse
-
.derive(seed, lightness: 50, saturation_range: (40..80)) ⇒ HSL
Generate a deterministic HSL color from an integer seed.
-
.parse(input) ⇒ HSL
Parse an HSL color from a string.
Instance Method Summary collapse
-
#==(other) ⇒ Boolean
Check if two HSL colors are equal.
-
#blend(other, amount = 0.5) ⇒ HSL
Blend this color with another color in HSL space.
-
#darken(amount = 0.1) ⇒ HSL
Create a darker version by decreasing lightness.
-
#initialize(hue:, saturation:, lightness:) ⇒ HSL
constructor
Create a new HSL color.
-
#lighten(amount = 0.1) ⇒ HSL
Create a lighter version by increasing lightness.
-
#luminance ⇒ Float
Calculate the relative luminance.
-
#progression(steps:, lightness:, saturation: nil) ⇒ Array<HSL>
Generate a progression of colors by varying lightness and saturation.
-
#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
Constructor Details
#initialize(hue:, saturation:, lightness:) ⇒ HSL
Create a new HSL color.
91 92 93 94 95 96 |
# File 'lib/unmagic/color/hsl.rb', line 91 def initialize(hue:, saturation:, lightness:) super() @hue = Color::Hue.new(value: hue) @saturation = Color::Saturation.new(saturation) @lightness = Color::Lightness.new(lightness) end |
Instance Attribute Details
#hue ⇒ Object (readonly)
Returns the value of attribute hue.
78 79 80 |
# File 'lib/unmagic/color/hsl.rb', line 78 def hue @hue end |
#lightness ⇒ Object (readonly)
Returns the value of attribute lightness.
78 79 80 |
# File 'lib/unmagic/color/hsl.rb', line 78 def lightness @lightness end |
#saturation ⇒ Object (readonly)
Returns the value of attribute saturation.
78 79 80 |
# File 'lib/unmagic/color/hsl.rb', line 78 def saturation @saturation end |
Class Method Details
.derive(seed, lightness: 50, saturation_range: (40..80)) ⇒ HSL
Generate a deterministic HSL color from an integer seed.
Creates visually distinct, consistent colors from hash values. Particularly useful because HSL naturally spreads colors evenly around the color wheel.
184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/unmagic/color/hsl.rb', line 184 def derive(seed, lightness: 50, saturation_range: (40..80)) raise ArgumentError, "Seed must be an integer" unless seed.is_a?(Integer) h32 = seed & 0xFFFFFFFF # Ensure 32-bit # Hue: distribute evenly across the color wheel h = (h32 % 360).to_f # Saturation: map a byte into the provided range s = saturation_range.begin + ((h32 >> 8) & 0xFF) / 255.0 * (saturation_range.end - saturation_range.begin) new(hue: h, saturation: s, lightness: lightness) end |
.parse(input) ⇒ HSL
Parse an HSL color from a string.
Accepts formats:
-
CSS format: “hsl(120, 100%, 50%)”
-
Raw values: “120, 100%, 50%” or “120, 100, 50”
-
Percentages optional for saturation and lightness
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 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 |
# File 'lib/unmagic/color/hsl.rb', line 115 def parse(input) raise ParseError, "Input must be a string" unless input.is_a?(::String) # Remove hsl() wrapper if present clean = input.gsub(/^hsl\s*\(\s*|\s*\)$/, "").strip # Split and parse values parts = clean.split(/\s*,\s*/) unless parts.length == 3 raise ParseError, "Expected 3 HSL values, got #{parts.length}" end # Check if hue is numeric h_str = parts[0].strip unless h_str.match?(/\A\d+(\.\d+)?\z/) raise ParseError, "Invalid hue value: #{h_str.inspect} (must be a number)" end # Check if saturation and lightness are numeric (with optional %) s_str = parts[1].gsub("%", "").strip l_str = parts[2].gsub("%", "").strip unless s_str.match?(/\A\d+(\.\d+)?\z/) raise ParseError, "Invalid saturation value: #{parts[1].inspect} (must be a number with optional %)" end unless l_str.match?(/\A\d+(\.\d+)?\z/) raise ParseError, "Invalid lightness value: #{parts[2].inspect} (must be a number with optional %)" end h = h_str.to_f s = s_str.to_f l = l_str.to_f # Validate ranges if h < 0 || h > 360 raise ParseError, "Hue must be between 0 and 360, got #{h}" end if s < 0 || s > 100 raise ParseError, "Saturation must be between 0 and 100, got #{s}" end if l < 0 || l > 100 raise ParseError, "Lightness must be between 0 and 100, got #{l}" end new(hue: h, saturation: s, lightness: l) end |
Instance Method Details
#==(other) ⇒ Boolean
Check if two HSL colors are equal.
298 299 300 301 302 303 |
# File 'lib/unmagic/color/hsl.rb', line 298 def ==(other) other.is_a?(Unmagic::Color::HSL) && lightness == other.lightness && saturation == other.saturation && hue == other.hue end |
#blend(other, amount = 0.5) ⇒ HSL
Blend this color with another color in HSL space.
Blending in HSL can produce different results than RGB blending, often creating more natural-looking color transitions.
248 249 250 251 252 253 254 255 256 257 258 |
# File 'lib/unmagic/color/hsl.rb', line 248 def blend(other, amount = 0.5) amount = amount.to_f.clamp(0, 1) other_hsl = other.respond_to?(:to_hsl) ? other.to_hsl : other # Blend in HSL space new_hue = @hue.value * (1 - amount) + other_hsl.hue.value * amount new_saturation = @saturation.value * (1 - amount) + other_hsl.saturation.value * amount new_lightness = @lightness.value * (1 - amount) + other_hsl.lightness.value * amount Unmagic::Color::HSL.new(hue: new_hue, saturation: new_saturation, lightness: new_lightness) end |
#darken(amount = 0.1) ⇒ HSL
Create a darker version by decreasing lightness.
In HSL, darkening moves the color toward black while preserving the hue. The amount determines how much to reduce the current lightness toward 0%.
288 289 290 291 292 |
# File 'lib/unmagic/color/hsl.rb', line 288 def darken(amount = 0.1) amount = amount.to_f.clamp(0, 1) new_lightness = @lightness.value * (1 - amount) Unmagic::Color::HSL.new(hue: @hue.value, saturation: @saturation.value, lightness: new_lightness) end |
#lighten(amount = 0.1) ⇒ HSL
Create a lighter version by increasing lightness.
In HSL, lightening moves the color toward white while preserving the hue. The amount determines how far to move from the current lightness toward 100%.
271 272 273 274 275 |
# File 'lib/unmagic/color/hsl.rb', line 271 def lighten(amount = 0.1) amount = amount.to_f.clamp(0, 1) new_lightness = @lightness.value + (100 - @lightness.value) * amount Unmagic::Color::HSL.new(hue: @hue.value, saturation: @saturation.value, lightness: new_lightness) end |
#luminance ⇒ Float
Calculate the relative luminance.
Converts to RGB first, then calculates luminance.
231 232 233 |
# File 'lib/unmagic/color/hsl.rb', line 231 def luminance to_rgb.luminance end |
#progression(steps:, lightness:, saturation: nil) ⇒ Array<HSL>
Generate a progression of colors by varying lightness and saturation.
This creates an array of related colors, useful for color scales in UI design (like shades of blue from light to dark).
The lightness and saturation can be provided as:
-
Array: Specific values for each step (last value repeats if array is shorter)
-
Proc: Dynamic calculation based on the base color and step index
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 |
# File 'lib/unmagic/color/hsl.rb', line 331 def progression(steps:, lightness:, saturation: nil) raise ArgumentError, "steps must be at least 1" if steps < 1 raise ArgumentError, "lightness must be a proc or array" unless lightness.respond_to?(:call) || lightness.is_a?(Array) raise ArgumentError, "saturation must be a proc or array" if saturation && !saturation.respond_to?(:call) && !saturation.is_a?(Array) colors = [] (0...steps).each do |i| # Calculate new lightness using the provided proc or array new_lightness = if lightness.is_a?(Array) # Use array value at index i, or last value if beyond array length lightness[i] || lightness.last else lightness.call(self, i) end new_lightness = new_lightness.to_f.clamp(0, 100) # Calculate new saturation using the provided proc/array or keep current new_saturation = if saturation if saturation.is_a?(Array) # Use array value at index i, or last value if beyond array length (saturation[i] || saturation.last).to_f.clamp(0, 100) else saturation.call(self, i).to_f.clamp(0, 100) end else @saturation.value end # Create new HSL color with computed values color = self.class.new(hue: @hue.value, saturation: new_saturation, lightness: new_lightness) colors << color end colors end |
#to_hsl ⇒ HSL
Convert to HSL color space.
Since this is already an HSL color, returns self.
204 205 206 |
# File 'lib/unmagic/color/hsl.rb', line 204 def to_hsl self end |
#to_oklch ⇒ OKLCH
Convert to OKLCH color space.
Converts via RGB as an intermediate step.
222 223 224 |
# File 'lib/unmagic/color/hsl.rb', line 222 def to_oklch to_rgb.to_oklch end |
#to_rgb ⇒ RGB
Convert to RGB color space.
211 212 213 214 215 |
# File 'lib/unmagic/color/hsl.rb', line 211 def to_rgb rgb = hsl_to_rgb require_relative "rgb" Unmagic::Color::RGB.new(red: rgb[0], green: rgb[1], blue: rgb[2]) end |
#to_s ⇒ String
Convert to string representation.
Returns the CSS hsl() function format.
378 379 380 |
# File 'lib/unmagic/color/hsl.rb', line 378 def to_s "hsl(#{@hue.value.round}, #{@saturation.value}%, #{@lightness.value}%)" end |