red-colors 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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