Class: Philiprehberger::ColorConvert::Color

Inherits:
Object
  • Object
show all
Defined in:
lib/philiprehberger/color_convert/color.rb

Overview

Represents a color with conversion, manipulation, and comparison methods.

Constant Summary collapse

COLOR_BLINDNESS_MATRICES =

Color blindness simulation matrices (Brettel/Vienot method).

{
  protanopia: [
    [0.152286, 1.052583, -0.204868],
    [0.114503, 0.786281, 0.099216],
    [-0.003882, -0.048116, 1.051998]
  ],
  deuteranopia: [
    [0.367322, 0.860646, -0.227968],
    [0.280085, 0.672501, 0.047413],
    [-0.011820, 0.042940, 0.968881]
  ],
  tritanopia: [
    [1.255528, -0.076749, -0.178779],
    [-0.078411, 0.930809, 0.147602],
    [0.004733, 0.691367, 0.303900]
  ]
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(r, g, b) ⇒ Color

Returns a new instance of Color.

Parameters:

  • r (Integer)

    red component (0-255)

  • g (Integer)

    green component (0-255)

  • b (Integer)

    blue component (0-255)



19
20
21
22
23
# File 'lib/philiprehberger/color_convert/color.rb', line 19

def initialize(r, g, b)
  @r = clamp(r.round, 0, 255)
  @g = clamp(g.round, 0, 255)
  @b = clamp(b.round, 0, 255)
end

Instance Attribute Details

#bInteger (readonly)

Returns blue component (0-255).

Returns:

  • (Integer)

    blue component (0-255)



14
15
16
# File 'lib/philiprehberger/color_convert/color.rb', line 14

def b
  @b
end

#gInteger (readonly)

Returns green component (0-255).

Returns:

  • (Integer)

    green component (0-255)



11
12
13
# File 'lib/philiprehberger/color_convert/color.rb', line 11

def g
  @g
end

#rInteger (readonly)

Returns red component (0-255).

Returns:

  • (Integer)

    red component (0-255)



8
9
10
# File 'lib/philiprehberger/color_convert/color.rb', line 8

def r
  @r
end

Class Method Details

.delinearize_srgb_class(c) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



488
489
490
491
492
493
494
# File 'lib/philiprehberger/color_convert/color.rb', line 488

def self.delinearize_srgb_class(c)
  if c <= 0.0031308
    12.92 * c
  else
    (1.055 * (c**(1.0 / 2.4))) - 0.055
  end
end

.from_cmyk(c, m, y, k) ⇒ Color

Create a Color from CMYK values.

Parameters:

  • c (Numeric)

    cyan (0-100)

  • m (Numeric)

    magenta (0-100)

  • y (Numeric)

    yellow (0-100)

  • k (Numeric)

    key/black (0-100)

Returns:



409
410
411
412
413
414
415
416
417
418
419
420
# File 'lib/philiprehberger/color_convert/color.rb', line 409

def self.from_cmyk(c, m, y, k)
  c /= 100.0
  m /= 100.0
  y /= 100.0
  k /= 100.0

  r = 255 * (1 - c) * (1 - k)
  g = 255 * (1 - m) * (1 - k)
  b = 255 * (1 - y) * (1 - k)

  new(r.round, g.round, b.round)
end

.from_hsl(h, s, l) ⇒ Color

Create a Color from HSL values.

Parameters:

  • h (Numeric)

    hue (0-360)

  • s (Numeric)

    saturation (0-100)

  • l (Numeric)

    lightness (0-100)

Returns:



382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/philiprehberger/color_convert/color.rb', line 382

def self.from_hsl(h, s, l)
  h /= 360.0
  s /= 100.0
  l /= 100.0

  if s.zero?
    val = (l * 255).round
    return new(val, val, val)
  end

  q = l < 0.5 ? l * (1 + s) : l + s - (l * s)
  p = (2 * l) - q

  r = hue_to_rgb(p, q, h + (1.0 / 3))
  g = hue_to_rgb(p, q, h)
  b = hue_to_rgb(p, q, h - (1.0 / 3))

  new((r * 255).round, (g * 255).round, (b * 255).round)
end

.from_lab(l, a, b) ⇒ Color

Create a Color from CIELAB values (D65 illuminant).

Parameters:

  • l (Numeric)

    lightness (0-100)

  • a (Numeric)

    green-red component (approx -128 to 127)

  • b (Numeric)

    blue-yellow component (approx -128 to 127)

Returns:



428
429
430
431
432
433
434
435
436
437
438
439
# File 'lib/philiprehberger/color_convert/color.rb', line 428

def self.from_lab(l, a, b)
  # LAB to XYZ
  fy = (l + 16.0) / 116.0
  fx = (a / 500.0) + fy
  fz = fy - (b / 200.0)

  x = lab_f_inv(fx) * 95.047
  y = lab_f_inv(fy) * 100.0
  z = lab_f_inv(fz) * 108.883

  from_xyz(x, y, z)
end

.from_xyz(x, y, z) ⇒ Color

Create a Color from CIE XYZ values.

Parameters:

  • x (Numeric)

    X component

  • y (Numeric)

    Y component

  • z (Numeric)

    Z component

Returns:



447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
# File 'lib/philiprehberger/color_convert/color.rb', line 447

def self.from_xyz(x, y, z)
  x /= 100.0
  y /= 100.0
  z /= 100.0

  r = (x * 3.2404542) + (y * -1.5371385) + (z * -0.4985314)
  g = (x * -0.9692660) + (y * 1.8760108) + (z * 0.0415560)
  b = (x * 0.0556434) + (y * -0.2040259) + (z * 1.0572252)

  r = delinearize_srgb_class(r)
  g = delinearize_srgb_class(g)
  b = delinearize_srgb_class(b)

  new(
    [[r * 255, 0].max, 255].min.round,
    [[g * 255, 0].max, 255].min.round,
    [[b * 255, 0].max, 255].min.round
  )
end

.hue_to_rgb(p, q, t) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



468
469
470
471
472
473
474
475
476
# File 'lib/philiprehberger/color_convert/color.rb', line 468

def self.hue_to_rgb(p, q, t)
  t += 1 if t.negative?
  t -= 1 if t > 1
  return p + ((q - p) * 6 * t) if t < 1.0 / 6
  return q if t < 1.0 / 2
  return p + ((q - p) * ((2.0 / 3) - t) * 6) if t < 2.0 / 3

  p
end

.lab_f_inv(t) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



479
480
481
482
483
484
485
# File 'lib/philiprehberger/color_convert/color.rb', line 479

def self.lab_f_inv(t)
  if t > 6.0 / 29
    t**3
  else
    3.0 * ((6.0 / 29)**2) * (t - (4.0 / 29))
  end
end

Instance Method Details

#==(other) ⇒ Boolean

Returns:

  • (Boolean)


372
373
374
# File 'lib/philiprehberger/color_convert/color.rb', line 372

def ==(other)
  other.is_a?(Color) && @r == other.r && @g == other.g && @b == other.b
end

#analogousArray<Color>

Generate analogous colors (30 degrees apart on the color wheel).

Returns:

  • (Array<Color>)

    array of 3 colors: -30deg, self, +30deg



215
216
217
218
219
220
221
222
# File 'lib/philiprehberger/color_convert/color.rb', line 215

def analogous
  hsl = to_hsl
  [
    self.class.from_hsl((hsl[:h] - 30) % 360, hsl[:s], hsl[:l]),
    self.class.new(@r, @g, @b),
    self.class.from_hsl((hsl[:h] + 30) % 360, hsl[:s], hsl[:l])
  ]
end

#blend(other, weight: 0.5) ⇒ Color

Blend this color with another color.

Parameters:

  • other (Color)

    the other color to blend with

  • weight (Float) (defaults to: 0.5)

    blend weight (0.0 = all self, 1.0 = all other, 0.5 = equal mix)

Returns:

  • (Color)

    the blended color



204
205
206
207
208
209
210
# File 'lib/philiprehberger/color_convert/color.rb', line 204

def blend(other, weight: 0.5)
  w = clamp(weight.to_f, 0.0, 1.0)
  new_r = (@r * (1.0 - w)) + (other.r * w)
  new_g = (@g * (1.0 - w)) + (other.g * w)
  new_b = (@b * (1.0 - w)) + (other.b * w)
  self.class.new(new_r.round, new_g.round, new_b.round)
end

#complementColor

Return the complementary color (180 degrees on the color wheel).

Returns:

  • (Color)

    the complement color



193
194
195
196
197
# File 'lib/philiprehberger/color_convert/color.rb', line 193

def complement
  hsl = to_hsl
  new_h = (hsl[:h] + 180) % 360
  self.class.from_hsl(new_h, hsl[:s], hsl[:l])
end

#contrast_ratio(other) ⇒ Float

Calculate the WCAG contrast ratio between this color and another.

Parameters:

  • other (Color)

    the other color

Returns:

  • (Float)

    the contrast ratio (1.0 to 21.0)



323
324
325
326
327
328
329
# File 'lib/philiprehberger/color_convert/color.rb', line 323

def contrast_ratio(other)
  l1 = relative_luminance
  l2 = other.relative_luminance
  lighter = [l1, l2].max
  darker = [l1, l2].min
  ((lighter + 0.05) / (darker + 0.05)).round(2)
end

#cool?Boolean

Returns true if the color temperature is cool.

Returns:

  • (Boolean)

    true if the color temperature is cool



362
363
364
# File 'lib/philiprehberger/color_convert/color.rb', line 362

def cool?
  temperature == :cool
end

#darken(amount) ⇒ Color

Darken the color by a percentage.

Parameters:

  • amount (Numeric)

    percentage to darken (0-100)

Returns:

  • (Color)

    a new darkened color



168
169
170
# File 'lib/philiprehberger/color_convert/color.rb', line 168

def darken(amount)
  lighten(-amount)
end

#desaturate(amount) ⇒ Color

Decrease saturation by a percentage.

Parameters:

  • amount (Numeric)

    percentage to decrease saturation (0-100)

Returns:

  • (Color)

    a new desaturated color



186
187
188
# File 'lib/philiprehberger/color_convert/color.rb', line 186

def desaturate(amount)
  saturate(-amount)
end

#gradient(other, steps: 5) ⇒ Array<Color>

Generate a gradient palette between this color and another.

Parameters:

  • other (Color)

    the target color

  • steps (Integer) (defaults to: 5)

    number of colors in the gradient (minimum 2)

Returns:

  • (Array<Color>)

    array of colors forming a gradient



297
298
299
300
301
302
303
# File 'lib/philiprehberger/color_convert/color.rb', line 297

def gradient(other, steps: 5)
  steps = [steps, 2].max
  (0...steps).map do |i|
    weight = i / (steps - 1).to_f
    blend(other, weight: weight)
  end
end

#lighten(amount) ⇒ Color

Lighten the color by a percentage.

Parameters:

  • amount (Numeric)

    percentage to lighten (0-100)

Returns:

  • (Color)

    a new lightened color



158
159
160
161
162
# File 'lib/philiprehberger/color_convert/color.rb', line 158

def lighten(amount)
  hsl = to_hsl
  new_l = clamp(hsl[:l] + amount, 0, 100)
  self.class.from_hsl(hsl[:h], hsl[:s], new_l)
end

#monochromatic(steps: 5) ⇒ Array<Color>

Generate a monochromatic palette by varying lightness.

Parameters:

  • steps (Integer) (defaults to: 5)

    number of shades to generate (minimum 2)

Returns:

  • (Array<Color>)

    array of colors from dark to light



309
310
311
312
313
314
315
316
317
# File 'lib/philiprehberger/color_convert/color.rb', line 309

def monochromatic(steps: 5)
  steps = [steps, 2].max
  hsl = to_hsl
  step_size = 100.0 / (steps + 1)

  (1..steps).map do |i|
    self.class.from_hsl(hsl[:h], hsl[:s], step_size * i)
  end
end

#relative_luminanceFloat

Calculate relative luminance per WCAG 2.0.

Returns:

  • (Float)

    luminance value (0.0 to 1.0)



334
335
336
337
338
339
# File 'lib/philiprehberger/color_convert/color.rb', line 334

def relative_luminance
  rs = linearize(@r / 255.0)
  gs = linearize(@g / 255.0)
  bs = linearize(@b / 255.0)
  (0.2126 * rs) + (0.7152 * gs) + (0.0722 * bs)
end

#saturate(amount) ⇒ Color

Increase saturation by a percentage.

Parameters:

  • amount (Numeric)

    percentage to increase saturation (0-100)

Returns:

  • (Color)

    a new saturated color



176
177
178
179
180
# File 'lib/philiprehberger/color_convert/color.rb', line 176

def saturate(amount)
  hsl = to_hsl
  new_s = clamp(hsl[:s] + amount, 0, 100)
  self.class.from_hsl(hsl[:h], new_s, hsl[:l])
end

#simulate_color_blindness(type) ⇒ Color

Simulate color blindness.

Parameters:

  • type (Symbol)

    one of :protanopia, :deuteranopia, :tritanopia

Returns:

  • (Color)

    the simulated color

Raises:

  • (ArgumentError)

    if type is not recognized



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/philiprehberger/color_convert/color.rb', line 266

def simulate_color_blindness(type)
  matrix = COLOR_BLINDNESS_MATRICES[type]
  raise ArgumentError, "Unknown color blindness type: #{type}" unless matrix

  rf = @r / 255.0
  gf = @g / 255.0
  bf = @b / 255.0

  # Convert to linear RGB
  rl = linearize_srgb(rf)
  gl = linearize_srgb(gf)
  bl = linearize_srgb(bf)

  # Apply simulation matrix
  new_r = (matrix[0][0] * rl) + (matrix[0][1] * gl) + (matrix[0][2] * bl)
  new_g = (matrix[1][0] * rl) + (matrix[1][1] * gl) + (matrix[1][2] * bl)
  new_b = (matrix[2][0] * rl) + (matrix[2][1] * gl) + (matrix[2][2] * bl)

  # Convert back to sRGB
  new_r = delinearize_srgb(clamp(new_r, 0.0, 1.0))
  new_g = delinearize_srgb(clamp(new_g, 0.0, 1.0))
  new_b = delinearize_srgb(clamp(new_b, 0.0, 1.0))

  self.class.new((new_r * 255).round, (new_g * 255).round, (new_b * 255).round)
end

#split_complementaryArray<Color>

Generate split-complementary colors (150 and 210 degrees from base).

Returns:

  • (Array<Color>)

    array of 3 colors



252
253
254
255
256
257
258
259
# File 'lib/philiprehberger/color_convert/color.rb', line 252

def split_complementary
  hsl = to_hsl
  [
    self.class.new(@r, @g, @b),
    self.class.from_hsl((hsl[:h] + 150) % 360, hsl[:s], hsl[:l]),
    self.class.from_hsl((hsl[:h] + 210) % 360, hsl[:s], hsl[:l])
  ]
end

#temperatureSymbol

Classify the color temperature based on HSL hue.

Returns:

  • (Symbol)

    :warm, :cool, or :neutral



344
345
346
347
348
349
350
351
352
353
354
# File 'lib/philiprehberger/color_convert/color.rb', line 344

def temperature
  hue = to_hsl[:h]

  if hue <= 60 || hue >= 300
    :warm
  elsif hue.between?(120, 240)
    :cool
  else
    :neutral
  end
end

#tetradicArray<Color>

Generate tetradic (rectangular) colors (90 degrees apart).

Returns:

  • (Array<Color>)

    array of 4 colors



239
240
241
242
243
244
245
246
247
# File 'lib/philiprehberger/color_convert/color.rb', line 239

def tetradic
  hsl = to_hsl
  [
    self.class.new(@r, @g, @b),
    self.class.from_hsl((hsl[:h] + 90) % 360, hsl[:s], hsl[:l]),
    self.class.from_hsl((hsl[:h] + 180) % 360, hsl[:s], hsl[:l]),
    self.class.from_hsl((hsl[:h] + 270) % 360, hsl[:s], hsl[:l])
  ]
end

#to_cmykHash

Convert to CMYK hash.

Returns:

  • (Hash)

    with :c, :m, :y, :k keys (0-100)



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/philiprehberger/color_convert/color.rb', line 101

def to_cmyk
  rf = @r / 255.0
  gf = @g / 255.0
  bf = @b / 255.0

  k = 1.0 - [rf, gf, bf].max

  if k >= 1.0
    return { c: 0.0, m: 0.0, y: 0.0, k: 100.0 }
  end

  c = (1.0 - rf - k) / (1.0 - k)
  m = (1.0 - gf - k) / (1.0 - k)
  y = (1.0 - bf - k) / (1.0 - k)

  { c: (c * 100).round(1), m: (m * 100).round(1), y: (y * 100).round(1), k: (k * 100).round(1) }
end

#to_hexString

Convert to hex string.

Returns:

  • (String)

    hex color string (e.g., “#ff0000”)



28
29
30
# File 'lib/philiprehberger/color_convert/color.rb', line 28

def to_hex
  format('#%<r>02x%<g>02x%<b>02x', r: @r, g: @g, b: @b)
end

#to_hslHash

Convert to HSL hash.

Returns:

  • (Hash)

    with :h (0-360), :s (0-100), :l (0-100) keys



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/philiprehberger/color_convert/color.rb', line 42

def to_hsl
  rf = @r / 255.0
  gf = @g / 255.0
  bf = @b / 255.0

  max = [rf, gf, bf].max
  min = [rf, gf, bf].min
  l = (max + min) / 2.0

  if max == min
    h = 0.0
    s = 0.0
  else
    d = max - min
    s = l > 0.5 ? d / (2.0 - max - min) : d / (max + min)
    h = case max
        when rf then ((gf - bf) / d) + (gf < bf ? 6 : 0)
        when gf then ((bf - rf) / d) + 2
        else ((rf - gf) / d) + 4
        end
    h /= 6.0
  end

  { h: (h * 360).round(1), s: (s * 100).round(1), l: (l * 100).round(1) }
end

#to_hsvHash

Convert to HSV hash.

Returns:

  • (Hash)

    with :h (0-360), :s (0-100), :v (0-100) keys



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/philiprehberger/color_convert/color.rb', line 71

def to_hsv
  rf = @r / 255.0
  gf = @g / 255.0
  bf = @b / 255.0

  max = [rf, gf, bf].max
  min = [rf, gf, bf].min
  d = max - min

  v = max

  s = max.zero? ? 0.0 : d / max

  if max == min
    h = 0.0
  else
    h = case max
        when rf then ((gf - bf) / d) + (gf < bf ? 6 : 0)
        when gf then ((bf - rf) / d) + 2
        else ((rf - gf) / d) + 4
        end
    h /= 6.0
  end

  { h: (h * 360).round(1), s: (s * 100).round(1), v: (v * 100).round(1) }
end

#to_labHash

Convert to CIELAB hash via XYZ (D65 illuminant).

Returns:

  • (Hash)

    with :l (0-100), :a (approx -128 to 127), :b (approx -128 to 127) keys



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/philiprehberger/color_convert/color.rb', line 122

def to_lab
  xyz = to_xyz
  x = xyz[:x] / 95.047
  y = xyz[:y] / 100.0
  z = xyz[:z] / 108.883

  x = lab_f(x)
  y = lab_f(y)
  z = lab_f(z)

  l = (116.0 * y) - 16.0
  a = 500.0 * (x - y)
  b = 200.0 * (y - z)

  { l: l.round(2), a: a.round(2), b: b.round(2) }
end

#to_rgbHash

Convert to RGB hash.

Returns:

  • (Hash)

    with :r, :g, :b keys (0-255)



35
36
37
# File 'lib/philiprehberger/color_convert/color.rb', line 35

def to_rgb
  { r: @r, g: @g, b: @b }
end

#to_sString

Returns:

  • (String)


367
368
369
# File 'lib/philiprehberger/color_convert/color.rb', line 367

def to_s
  to_hex
end

#to_xyzHash

Convert to CIE XYZ color space (D65 illuminant).

Returns:

  • (Hash)

    with :x, :y, :z keys



142
143
144
145
146
147
148
149
150
151
152
# File 'lib/philiprehberger/color_convert/color.rb', line 142

def to_xyz
  rf = linearize_srgb(@r / 255.0) * 100.0
  gf = linearize_srgb(@g / 255.0) * 100.0
  bf = linearize_srgb(@b / 255.0) * 100.0

  x = (rf * 0.4124564) + (gf * 0.3575761) + (bf * 0.1804375)
  y = (rf * 0.2126729) + (gf * 0.7151522) + (bf * 0.0721750)
  z = (rf * 0.0193339) + (gf * 0.1191920) + (bf * 0.9503041)

  { x: x.round(4), y: y.round(4), z: z.round(4) }
end

#triadicArray<Color>

Generate triadic colors (120 degrees apart on the color wheel).

Returns:

  • (Array<Color>)

    array of 3 colors



227
228
229
230
231
232
233
234
# File 'lib/philiprehberger/color_convert/color.rb', line 227

def triadic
  hsl = to_hsl
  [
    self.class.new(@r, @g, @b),
    self.class.from_hsl((hsl[:h] + 120) % 360, hsl[:s], hsl[:l]),
    self.class.from_hsl((hsl[:h] + 240) % 360, hsl[:s], hsl[:l])
  ]
end

#warm?Boolean

Returns true if the color temperature is warm.

Returns:

  • (Boolean)

    true if the color temperature is warm



357
358
359
# File 'lib/philiprehberger/color_convert/color.rb', line 357

def warm?
  temperature == :warm
end