red-colors 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ module Colors
2
+ module Helper
3
+ private def check_type(obj, type, name)
4
+ return obj if obj.instance_of?(type)
5
+ check_fail TypeError, "#{name} must be a #{type}, but #{obj.class} is given"
6
+ end
7
+
8
+ private def check_range(value, range, name)
9
+ return value if range.cover?(value)
10
+ check_fail ArgumentError, "#{name} must be in #{range}, but #{value} is given"
11
+ end
12
+
13
+ private def check_fail(exc_class, message)
14
+ raise exc_class, message, caller(2)
15
+ end
16
+ end
17
+ end
data/lib/colors/hsl.rb ADDED
@@ -0,0 +1,128 @@
1
+ module Colors
2
+ class HSL < AbstractColor
3
+ include Helper
4
+
5
+ def initialize(h, s, l)
6
+ @h, @s, @l = canonicalize(h, s, l)
7
+ end
8
+
9
+ attr_reader :h, :s, :l
10
+
11
+ def components
12
+ [h, s, l]
13
+ end
14
+
15
+ alias hsl_components components
16
+
17
+ def h=(h)
18
+ @h = Rational(h) % 360
19
+ end
20
+
21
+ def s=(s)
22
+ @s = if s.instance_of?(Integer)
23
+ check_range(s, 0..255, :s) / 255r
24
+ else
25
+ Rational(check_range(s, 0..1, :s))
26
+ end
27
+ end
28
+
29
+ def l=(l)
30
+ @l = if l.instance_of?(Integer)
31
+ check_range(l, 0..255, :l) / 255r
32
+ else
33
+ Rational(check_range(l, 0..1, :l))
34
+ end
35
+ end
36
+
37
+ alias hue h
38
+ alias saturation s
39
+ alias lightness l
40
+
41
+ alias hue= h=
42
+ alias saturation= s=
43
+ alias lightness= l=
44
+
45
+ def ==(other)
46
+ case other
47
+ when HSLA
48
+ other == self
49
+ when HSL
50
+ h == other.h && s == other.s && l == other.l
51
+ else
52
+ super
53
+ end
54
+ end
55
+
56
+ def desaturate(factor)
57
+ HSL.new(h, s*factor, l)
58
+ end
59
+
60
+ def to_hsl
61
+ self
62
+ end
63
+
64
+ def to_hsla(alpha: 1.0)
65
+ alpha = canonicalize_component(alpha, :alpha)
66
+ HSLA.new(h, s, l, alpha)
67
+ end
68
+
69
+ def to_rgb
70
+ RGB.new(*rgb_components)
71
+ end
72
+
73
+ def to_rgba(alpha: 1.0)
74
+ alpha = canonicalize_component(alpha, :alpha)
75
+ RGBA.new(*rgb_components, alpha)
76
+ end
77
+
78
+ def rgb_components
79
+ t2 = if l <= 0.5r
80
+ l * (s + 1r)
81
+ else
82
+ l + s - l * s
83
+ end
84
+ t1 = l * 2r - t2
85
+ hh = h/60r
86
+ r = hue_to_rgb(t1, t2, hh + 2)
87
+ g = hue_to_rgb(t1, t2, hh)
88
+ b = hue_to_rgb(t1, t2, hh - 2)
89
+ [r, g, b]
90
+ end
91
+
92
+ private def hue_to_rgb(t1, t2, h)
93
+ h += 6r if h < 0
94
+ h -= 6r if h >= 6
95
+ if h < 1
96
+ (t2 - t1) * h + t1
97
+ elsif h < 3
98
+ t2
99
+ elsif h < 4
100
+ (t2 - t1) * (4r - h) + t1
101
+ else
102
+ t1
103
+ end
104
+ end
105
+
106
+ private def canonicalize(h, s, l)
107
+ if [s, l].map(&:class) == [Integer, Integer]
108
+ canonicalize_from_integer(h, s, l)
109
+ else
110
+ [
111
+ Rational(h) % 360,
112
+ canonicalize_component_to_rational(s, :s),
113
+ canonicalize_component_to_rational(l, :l)
114
+ ]
115
+ end
116
+ end
117
+
118
+ private def canonicalize_from_integer(h, s, l)
119
+ check_type(s, Integer, :s)
120
+ check_type(l, Integer, :l)
121
+ [
122
+ Rational(h) % 360,
123
+ canonicalize_component_from_integer(s, :s),
124
+ canonicalize_component_from_integer(l, :l)
125
+ ]
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,81 @@
1
+ module Colors
2
+ class HSLA < HSL
3
+ def initialize(h, s, l, a)
4
+ @h, @s, @l, @a = canonicalize(h, s, l, a)
5
+ end
6
+
7
+ include AlphaComponent
8
+
9
+ def components
10
+ [h, s, l, a]
11
+ end
12
+
13
+ alias hsla_components components
14
+
15
+ def ==(other)
16
+ case other
17
+ when HSLA
18
+ h == other.h && s == other.s && l == other.l && a == other.a
19
+ when HSL
20
+ h == other.h && s == other.s && l == other.l && a == 1r
21
+ else
22
+ super
23
+ end
24
+ end
25
+
26
+ def desaturate(factor)
27
+ HSLA.new(h, s*factor, l, a)
28
+ end
29
+
30
+ def to_hsla
31
+ self
32
+ end
33
+
34
+ def to_rgba
35
+ RGBA.new(*rgb_components, a)
36
+ end
37
+
38
+ def to_hsl
39
+ if a == 1r
40
+ super
41
+ else
42
+ raise NotImplementedError,
43
+ "Unable to convert non-opaque HSLA to HSL"
44
+ end
45
+ end
46
+
47
+ def to_rgb
48
+ if a == 1r
49
+ super
50
+ else
51
+ raise NotImplementedError,
52
+ "Unable to convert non-opaque HSLA to RGB"
53
+ end
54
+ end
55
+
56
+ private def canonicalize(h, s, l, a)
57
+ if [s, l, a].map(&:class) == [Integer, Integer, Integer]
58
+ canonicalize_from_integer(h, s, l, a)
59
+ else
60
+ [
61
+ Rational(h) % 360,
62
+ canonicalize_component_to_rational(s, :s),
63
+ canonicalize_component_to_rational(l, :l),
64
+ canonicalize_component_to_rational(a, :a)
65
+ ]
66
+ end
67
+ end
68
+
69
+ private def canonicalize_from_integer(h, s, l, a)
70
+ check_type(s, Integer, :s)
71
+ check_type(l, Integer, :l)
72
+ check_type(a, Integer, :a)
73
+ [
74
+ Rational(h) % 360,
75
+ canonicalize_component_from_integer(s, :s),
76
+ canonicalize_component_from_integer(l, :l),
77
+ canonicalize_component_from_integer(a, :a)
78
+ ]
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,144 @@
1
+ require "numo/narray"
2
+
3
+ module Colors
4
+ # Human-friendly alternative to HSL color space.
5
+ # The definition of HUSL is provided in <http://www.hsluv.org>.
6
+ class HUSL < HSL
7
+ DEG2RAD = 0.01745329251994329577r # 2 * pi / 360
8
+
9
+ def self.from_rgb(r, g, b)
10
+ c = XYZ.from_rgb(r, g, b)
11
+ l, u, v = c.luv_components(WHITE_POINT_D65)
12
+ l, c, h = convert_luv_to_lch(l, u, v)
13
+ h, s, l = convert_lch_to_husl(l, c, h)
14
+ new(h, s.to_r.clamp(0r, 1r), l.to_r.clamp(0r, 1r))
15
+ end
16
+
17
+ private_class_method def self.convert_luv_to_lch(l, u, v)
18
+ c = Math.sqrt(u*u + v*v).to_r
19
+
20
+ if c < 1e-8
21
+ h = 0r
22
+ else
23
+ h = Math.atan2(v, u).to_r * 180/Math::PI.to_r
24
+ h += 360r if h < 0
25
+ end
26
+
27
+ [l, c, h]
28
+ end
29
+
30
+ private_class_method def self.convert_lch_to_husl(l, c, h)
31
+ if l > 99.9999999 || l < 1e-8
32
+ s = 0r
33
+ else
34
+ mx = max_chroma(l, h)
35
+ s = c / mx * 100r
36
+ end
37
+
38
+ h = 0r if c < 1e-8
39
+
40
+ [h, s/100r, l/100r]
41
+ end
42
+
43
+ def ==(other)
44
+ case other
45
+ when HUSL
46
+ h == other.h && s == other.s && l == other.l
47
+ else
48
+ other == self
49
+ end
50
+ end
51
+
52
+ def desaturate(factor)
53
+ to_rgb.desaturate(factor).to_husl
54
+ end
55
+
56
+ def to_husl
57
+ self
58
+ end
59
+
60
+ def to_rgb
61
+ RGB.new(*rgb_components)
62
+ end
63
+
64
+ def rgb_components
65
+ l, u, v = convert_lch_to_luv(*lch_components)
66
+ x, y, z = convert_luv_to_xyz(l, u, v)
67
+ XYZ.new(x, y, z).rgb_components
68
+ end
69
+
70
+ def lch_components
71
+ l = self.l * 100r
72
+ s = self.s * 100r
73
+
74
+ if l > 99.9999999 || l < 1e-8
75
+ c = 0r
76
+ else
77
+ mx = self.class.max_chroma(l, h)
78
+ c = mx / 100r * s
79
+ end
80
+
81
+ h = s < 1e-8 ? 0r : self.h
82
+
83
+ [l, c, h]
84
+ end
85
+
86
+ private def convert_lch_to_luv(l, c, h)
87
+ h_rad = h * DEG2RAD
88
+ u = Math.cos(h_rad).to_r * c
89
+ v = Math.sin(h_rad).to_r * c
90
+ [l, u, v]
91
+ end
92
+
93
+ private def convert_luv_to_xyz(l, u, v)
94
+ return [0r, 0r, 0r] if l <= 1e-8
95
+
96
+ wp_u, wp_v = WHITE_POINT_D65.uv_values
97
+ var_u = u / (13 * l) + wp_u
98
+ var_v = v / (13 * l) + wp_v
99
+ y = if l < 8
100
+ l / XYZ::KAPPA
101
+ else
102
+ ((l + 16r) / 116r)**3
103
+ end
104
+ x = -(9 * y * var_u) / ((var_u - 4) * var_v - var_u * var_v)
105
+ z = (9 * y - (15 * var_v * y) - (var_v * x)) / (3 * var_v)
106
+ [x, y, z]
107
+ end
108
+
109
+ def self.max_chroma(l, h)
110
+ h_rad = h * DEG2RAD
111
+ sin_h = Math.sin(h_rad).to_r
112
+ cos_h = Math.cos(h_rad).to_r
113
+
114
+ result = Float::INFINITY
115
+ get_bounds(l).each do |line|
116
+ len = line[1] / (sin_h - line[0] * cos_h)
117
+ result = len if 0 <= len && len < result
118
+ end
119
+ result
120
+ end
121
+
122
+ def self.get_bounds(l)
123
+ sub1 = (l + 16)**3 / 1560896r
124
+ sub2 = sub1 > XYZ::EPSILON ? sub1 : l/XYZ::KAPPA
125
+
126
+ bounds = Array.new(6) { [0r, 0r] }
127
+ 0.upto(2) do |ch|
128
+ m1 = XYZ2RGB[ch, 0].to_r
129
+ m2 = XYZ2RGB[ch, 1].to_r
130
+ m3 = XYZ2RGB[ch, 2].to_r
131
+
132
+ [0, 1].each do |t|
133
+ top1 = (284517r * m1 - 94839r * m3) * sub2
134
+ top2 = (838422r * m3 + 769860r * m2 + 731718r * m1) * l * sub2 - 769860r * t * l
135
+ bottom = (632260r * m3 - 126452r * m2) * sub2 + 126452r * t
136
+
137
+ bounds[ch*2 + t][0] = top1 / bottom
138
+ bounds[ch*2 + t][1] = top2 / bottom
139
+ end
140
+ end
141
+ bounds
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,114 @@
1
+ require_relative 'color_data'
2
+
3
+ module Colors
4
+ module NamedColors
5
+ class Mapping
6
+ def initialize
7
+ @mapping = {}
8
+ @cache = {}
9
+ end
10
+
11
+ attr_reader :cache
12
+
13
+ def [](name)
14
+ if NamedColors.nth_color?(name)
15
+ cycle = ColorData::DEFAULT_COLOR_CYCLE
16
+ name = cycle[name[1..-1].to_i % cycle.length]
17
+ end
18
+ if cache.has_key?(name)
19
+ cache[name]
20
+ else
21
+ cache[name] = lookup_no_color_cycle(name)
22
+ end
23
+ end
24
+
25
+ private def lookup_no_color_cycle(color)
26
+ orig_color = color
27
+ case color
28
+ when /\Anone\z/i
29
+ return RGBA.new(0, 0, 0, 0)
30
+ when String
31
+ # nothing to do
32
+ when Symbol
33
+ color = color.to_s
34
+ else
35
+ color = color.to_str
36
+ end
37
+ color = @mapping.fetch(color, color)
38
+ case color
39
+ when /\A#\h+\z/
40
+ case color.length - 1
41
+ when 3, 6
42
+ RGB.parse(color)
43
+ when 4, 8
44
+ RGBA.parse(color)
45
+ else
46
+ raise RuntimeError,
47
+ "[BUG] Invalid hex string form #{color.inspect} for #{name.inspect}"
48
+ end
49
+ when Array
50
+ case color.length
51
+ when 3
52
+ RGB.new(*color)
53
+ when 4
54
+ RGBA.new(*color)
55
+ else
56
+ raise RuntimeError,
57
+ "[BUG] Invalid number of color components #{color} for #{name.inspect}"
58
+ end
59
+ else
60
+ color
61
+ end
62
+ end
63
+
64
+ def []=(name, value)
65
+ @mapping[name] = value
66
+ ensure
67
+ cache.clear
68
+ end
69
+
70
+ def delete(name)
71
+ @mapping.delete(name)
72
+ ensure
73
+ cache.clear
74
+ end
75
+
76
+ def update(other)
77
+ @mapping.update(other)
78
+ ensure
79
+ cache.clear
80
+ end
81
+ end
82
+
83
+ MAPPING = Mapping.new
84
+ MAPPING.update(ColorData::XKCD_COLORS)
85
+ ColorData::XKCD_COLORS.each do |key, value|
86
+ MAPPING[key.sub("grey", "gray")] = value if key.include? "grey"
87
+ end
88
+ MAPPING.update(ColorData::CSS4_COLORS)
89
+ MAPPING.update(ColorData::TABLEAU_COLORS)
90
+ ColorData::TABLEAU_COLORS.each do |key, value|
91
+ MAPPING[key.sub("gray", "grey")] = value if key.include? "gray"
92
+ end
93
+ MAPPING.update(ColorData::BASE_COLORS)
94
+
95
+ def self.[](name)
96
+ MAPPING[name]
97
+ end
98
+
99
+ # Return whether `name` is an item in the color cycle.
100
+ def self.nth_color?(name)
101
+ case name
102
+ when String
103
+ # do nothing
104
+ when Symbol
105
+ name = name.to_s
106
+ else
107
+ name = name.to_str
108
+ end
109
+ name.match?(/\AC\d+\z/)
110
+ rescue NoMethodError, TypeError
111
+ false
112
+ end
113
+ end
114
+ end