Skip to content

Commit

Permalink
refactor: rewrite colorlib
Browse files Browse the repository at this point in the history
  • Loading branch information
anna328p committed Dec 17, 2023
1 parent 7233d6e commit dbe32db
Show file tree
Hide file tree
Showing 2 changed files with 189 additions and 77 deletions.
258 changes: 185 additions & 73 deletions lib/qbot/colorlib.rb
Original file line number Diff line number Diff line change
@@ -1,92 +1,204 @@
# frozen_string_literal: true

require 'matrix'

# Library for working with RGB, XYZ, and LAB colorspaces
module ColorLib
# rubocop: disable Layout/SpaceInsideArrayLiteralBrackets
RGB_XYZ_MATRIX = Matrix[
[ 0.4124564, 0.3575761, 0.1804375 ],
[ 0.2126729, 0.7151522, 0.0721750 ],
[ 0.0193339, 0.1191920, 0.9503041 ]
]

XYZ_RGB_MATRIX = Matrix[
[ 3.2404542, -1.5371385, -0.4985314 ],
[ -0.9692660, 1.8760108, 0.0415560 ],
[ 0.0556434, -0.2040259, 1.0572252 ]
]

D65 = [ 95.047, 100, 108.883 ].freeze
# rubocop: enable Layout/SpaceInsideArrayLiteralBrackets

def self.hex_to_rgb(hex) =
hex.chars.last(6).each_slice(2).map { _1.join.to_i(16) }

def self.rgb_to_hex(rgb) =
rgb.map { _1.round.clamp(0, 255).to_s(16).rjust(2, '0') }.join

def self.rgb_to_xyz(rgb)
r, g, b = rgb.map { _1 / 256.0 }
(RGB_XYZ_MATRIX * Matrix[[r], [g], [b]]).to_a.flatten
end
module QBot
# Library for working with RGB, XYZ, and LAB colorspaces
module ColorLib
def self.matrix_dim(mx)

Check warning on line 6 in lib/qbot/colorlib.rb

View check run for this annotation

Check Run Reporter / Lint / lint

com.puppycrawl.tools.checkstyle.Naming/MethodParameterName

com.puppycrawl.tools.checkstyle.Naming/MethodParameterName: Naming/MethodParameterName: Method parameter must be at least 3 characters long.
raise('argument is not a matrix') \
if mx.empty? || mx.first&.empty?

def self.xyz_to_rgb(xyz)
x, y, z = xyz
ary = (XYZ_RGB_MATRIX * Matrix[[x], [y], [z]]).to_a.flatten
ary.map { _1 * 256.0 }
end
raise('argument is not a matrix') \
unless mx.map(&:size).then { |sizes| sizes.all?(sizes.first) }

def self.scale_f(val)
delta = 6.0 / 29
t = val
mfirst = mx.first || []

if t > delta**3
t**(1.0 / 3)
else
t / (3 * delta**2) + 4.0 / 29
[mx.size, mfirst.size]
end
end

# rubocop: disable Metrics/AbcSize
def self.xyz_to_lab(xyz)
x, y, z = xyz.map { _1 * 100 }
xn, yn, zn = D65
def self.mul_compat?(mx1, mx2)
_, c1 = matrix_dim(mx1)
r2, = matrix_dim(mx2)

l = 116 * scale_f(y / yn) - 16
a = 500 * (scale_f(x / xn) - scale_f(y / yn))
b = 200 * (scale_f(y / yn) - scale_f(z / zn))
c1 == r2
end

[l, a, b]
end
def self.dot_product(ary1, ary2)
ary1.zip(ary2).map { |a, b| a * (b || 0.0) }.sum
end

def self.lab_to_xyz(lab)
l, a, b = lab
xn, yn, zn = D65
def self.matmul(mx1, mx2)
raise('matrices have incompatible dimensions') \
unless mul_compat?(mx1, mx2)

p = (l + 16) / 116.0
mx2t = mx2.transpose

x = xn * (p + a / 500.0)**3
y = yn * p**3
z = zn * (p - b / 200.0)**3
mx1.map { |row| mx2t.zip(row).map { |c, _| dot_product(row, c) } }
end

[x, y, z].map { _1 / 100.0 }
end
# rubocop: enable Metrics/AbcSize
# rubocop: disable Layout/SpaceInsideArrayLiteralBrackets
# rubocop: disable Layout/ExtraSpacing
RGB_XYZ_MATRIX = [
[ 0.4124564, 0.3575761, 0.1804375 ],
[ 0.2126729, 0.7151522, 0.0721750 ],
[ 0.0193339, 0.1191920, 0.9503041 ]
].freeze

XYZ_RGB_MATRIX = [
[ 3.2404542, -1.5371385, -0.4985314 ],
[ -0.9692660, 1.8760108, 0.0415560 ],
[ 0.0556434, -0.2040259, 1.0572252 ]
].freeze
# rubocop: enable Layout/ExtraSpacing
# rubocop: enable Layout/SpaceInsideArrayLiteralBrackets

##
# A tristimulus value in the CIE 1931 space.
class XYZTristimulus < Data.define(:x, :y, :z)

Check warning on line 56 in lib/qbot/colorlib.rb

View check run for this annotation

Check Run Reporter / Lint / lint

com.puppycrawl.tools.checkstyle.Style/DataInheritance

com.puppycrawl.tools.checkstyle.Style/DataInheritance: Style/DataInheritance: Don't extend an instance initialized by `Data.define`. Use a block to customize the class.
def to_srgb_linear
r, g, b = ColorLib.matmul(XYZ_RGB_MATRIX, [[x], [y], [z]]).flatten

SRGBLinearColor.new(r:, g:, b:)
end

def to_srgb
to_srgb_linear.gamma_compress
end

private def scale_f(val)
delta = 6.0 / 29
t = val

if t > delta**3
t**(1.0 / 3)
else
(t / (3 * (delta**2))) + (4.0 / 29)
end
end

# rubocop: disable Metrics/AbcSize
def to_cielab(illuminant: D65)
x_, y_, z_ = x * 100, y * 100, z * 100
xn, yn, zn = illuminant.x, illuminant.y, illuminant.z

l = (116 * scale_f(y_ / yn)) - 16
a = 500 * (scale_f(x_ / xn) - scale_f(y_ / yn))
b = 200 * (scale_f(y_ / yn) - scale_f(z_ / zn))

CIELABColor.new(l:, a:, b:)
end
# rubocop: enable Metrics/AbcSize

def to_ary = [x, y, z]
def to_a = to_ary
end

def self.cie76(lab1, lab2)
l1, a1, b1 = lab1
l2, a2, b2 = lab2
D65 = XYZTristimulus.new(x: 95.047, y: 100.0, z: 108.883)
D50 = XYZTristimulus.new(x: 96.42, y: 100.0, z: 82.51)

##
# An RGB value in the sRGB colorspace.
class SRGBColor < Data.define(:r, :g, :b)

Check warning on line 100 in lib/qbot/colorlib.rb

View check run for this annotation

Check Run Reporter / Lint / lint

com.puppycrawl.tools.checkstyle.Style/DataInheritance

com.puppycrawl.tools.checkstyle.Style/DataInheritance: Style/DataInheritance: Don't extend an instance initialized by `Data.define`. Use a block to customize the class.
def self.from_hex(hex)
r, g, b = hex.chars.last(6).each_slice(2)
.map { |c| c.join.to_i(16) / 255.0 }

new(r: r || 0.0, g: g || 0.0, b: b || 0.0)
end

def to_hex
[r, g, b].map { |val|
(val * 255).round.clamp(0, 255).to_s(16).rjust(2, '0')
}.join
end

private def gamma_expand_one(val)
if val <= 0.04045
val / 12.92
else
((val + 0.055) / 1.055)**2.4
end
end

def to_xyz
gamma_expand.to_xyz
end

def gamma_expand
SRGBLinearColor.new(
r: gamma_expand_one(r),
g: gamma_expand_one(g),
b: gamma_expand_one(b)
)
end

def to_ary = [r, g, b]
def to_a = to_ary
end

((l2 - l1)**2 + (a2 - a1)**2 + (b2 - b1)**2)**0.5
end
##
# A gamma-expanded ("linear light") RGB value in the sRGB colorspace.
class SRGBLinearColor < Data.define(:r, :g, :b)

Check warning on line 140 in lib/qbot/colorlib.rb

View check run for this annotation

Check Run Reporter / Lint / lint

com.puppycrawl.tools.checkstyle.Style/DataInheritance

com.puppycrawl.tools.checkstyle.Style/DataInheritance: Style/DataInheritance: Don't extend an instance initialized by `Data.define`. Use a block to customize the class.
def to_xyz
x, y, z = ColorLib.matmul(RGB_XYZ_MATRIX, [[r], [g], [b]]).flatten
XYZTristimulus.new(x:, y:, z:)
end

private def gamma_compress_one(val)
if val <= 0.0031308
12.92 * val
else
(1.055 * (val**(1.0 / 2.4))) - 0.055
end
end

def gamma_compress
SRGBColor.new(
r: gamma_compress_one(r),
g: gamma_compress_one(g),
b: gamma_compress_one(b)
)
end

def to_ary = [r, g, b]
def to_a = to_ary
end

def self.ciede2000(lab1, lab2)
# TODO: write this
cie76(lab1, lab2)
end
##
# A color in the CIE LAB color space.
class CIELABColor < Data.define(:l, :a, :b)

Check warning on line 168 in lib/qbot/colorlib.rb

View check run for this annotation

Check Run Reporter / Lint / lint

com.puppycrawl.tools.checkstyle.Style/DataInheritance

com.puppycrawl.tools.checkstyle.Style/DataInheritance: Style/DataInheritance: Don't extend an instance initialized by `Data.define`. Use a block to customize the class.
def self.from_hex(...)
SRGBColor.from_hex(...).to_xyz.to_cielab
end

def to_hex
to_xyz.to_srgb.to_hex
end

# rubocop: disable Metrics/AbcSize
def to_xyz(illuminant: D65)
xn, yn, zn = illuminant.x, illuminant.y, illuminant.z

p = (l + 16) / 116.0

def self.hex_to_lab(hex) = xyz_to_lab(rgb_to_xyz(hex_to_rgb(hex)))
x = xn * ((p + (a / 500.0))**3) / 100.0
y = yn * (p**3) / 100.0
z = zn * ((p - (b / 200.0))**3) / 100.0

def self.lab_to_hex(lab) = rgb_to_hex(xyz_to_rgb(lab_to_xyz(lab)))
XYZTristimulus.new(x:, y:, z:)
end
# rubocop: enable Metrics/AbcSize

def cie76(other)
(((other.l - l)**2) + ((other.a - a)**2) + ((other.b - b)**2))**0.5
end

def ciede2000(other)
# TODO: write this
cie76(other)
end

def to_ary = [l, a, b]
def to_a = to_ary
end
end
end
8 changes: 4 additions & 4 deletions modules/colors/wrapped_color_role.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ def self.search(server, query)
# Returns the color role with the closest color to the one given,
# using the CIE76 distance metric in CIELAB color space
def self.find_closest_on(server, target_hex)
target = ColorLib.hex_to_lab(target_hex.remove_prefix('#'))
target = ColorLib::CIELABColor.from_hex(target_hex)

self.for(server).min_by { |cur|
compare = ColorLib.hex_to_lab(cur.hex_bare)
ColorLib.cie76(target, compare)
compare = ColorLib::CIELABColor.from_hex(cur.hex_bare)
target.cie76(compare)
}
end

Expand Down Expand Up @@ -110,7 +110,7 @@ def self.color_ring(lightness, radius, count)
a = radius * Math.cos(this_angle)
b = radius * Math.sin(this_angle)

ColorLib.lab_to_hex [lightness, a, b]
ColorLib::CIELABColor(l: lightness, a:, b:).to_hex
end
end

Expand Down

0 comments on commit dbe32db

Please sign in to comment.