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.
- checksums.yaml +7 -0
- data/.yardopts +6 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +7 -0
- data/README.md +31 -0
- data/Rakefile +19 -0
- data/lib/colors.rb +30 -0
- data/lib/colors/abstract_color.rb +28 -0
- data/lib/colors/alpha_component.rb +13 -0
- data/lib/colors/color_data.rb +1146 -0
- data/lib/colors/helper.rb +17 -0
- data/lib/colors/hsl.rb +128 -0
- data/lib/colors/hsla.rb +81 -0
- data/lib/colors/husl.rb +144 -0
- data/lib/colors/named_colors.rb +114 -0
- data/lib/colors/rgb.rb +145 -0
- data/lib/colors/rgba.rb +101 -0
- data/lib/colors/version.rb +3 -0
- data/lib/colors/xyz.rb +123 -0
- data/red-colors.gemspec +41 -0
- data/test/helper.rb +13 -0
- data/test/run.rb +17 -0
- data/test/test-hsl.rb +267 -0
- data/test/test-hsla.rb +309 -0
- data/test/test-husl.rb +241 -0
- data/test/test-named-color.rb +43 -0
- data/test/test-rgb.rb +294 -0
- data/test/test-rgba.rb +332 -0
- data/test/test-xyz.rb +10 -0
- metadata +165 -0
data/lib/colors/rgb.rb
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
|
3
|
+
module Colors
|
4
|
+
class RGB < AbstractColor
|
5
|
+
include Helper
|
6
|
+
|
7
|
+
def self.parse(hex_string)
|
8
|
+
case hex_string.to_str.match(/\A#(\h+)\z/) { $1 }.length
|
9
|
+
when 3 # rgb
|
10
|
+
r, g, b = hex_string.scan(/\h/).map {|h| h.hex * 17 }
|
11
|
+
new(r, g, b)
|
12
|
+
when 6 # rrggbb
|
13
|
+
r, g, b = hex_string.scan(/\h{2}/).map(&:hex)
|
14
|
+
new(r, g, b)
|
15
|
+
else
|
16
|
+
raise ArgumentError, "Invalid hex string: #{hex_string.inspect}"
|
17
|
+
end
|
18
|
+
rescue NoMethodError
|
19
|
+
raise ArgumentError, "hex_string must be a hexadecimal string of `#rrggbb` or `#rgb` form"
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(r, g, b)
|
23
|
+
@r, @g, @b = canonicalize(r, g, b)
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :r, :g, :b
|
27
|
+
|
28
|
+
def components
|
29
|
+
[r, g, b]
|
30
|
+
end
|
31
|
+
|
32
|
+
alias rgb_components components
|
33
|
+
|
34
|
+
def r=(r)
|
35
|
+
@r = canonicalize_component(r, :r)
|
36
|
+
end
|
37
|
+
|
38
|
+
def g=(g)
|
39
|
+
@g = canonicalize_component(g, :g)
|
40
|
+
end
|
41
|
+
|
42
|
+
def b=(b)
|
43
|
+
@b = canonicalize_component(b, :b)
|
44
|
+
end
|
45
|
+
|
46
|
+
alias red r
|
47
|
+
alias green g
|
48
|
+
alias blue b
|
49
|
+
|
50
|
+
alias red= r=
|
51
|
+
alias green= g=
|
52
|
+
alias blue= b=
|
53
|
+
|
54
|
+
def ==(other)
|
55
|
+
case other
|
56
|
+
when RGBA
|
57
|
+
other == self
|
58
|
+
when RGB
|
59
|
+
r == other.r && g == other.g && b == other.b
|
60
|
+
else
|
61
|
+
super
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def desaturate(factor)
|
66
|
+
to_hsl.desaturate(factor).to_rgb
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_hex_string
|
70
|
+
"##{components.map {|c| "%02x" % (255*c).round.to_i }.join('')}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_rgb
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_rgba(alpha: 1.0)
|
78
|
+
alpha = canonicalize_component(alpha, :alpha)
|
79
|
+
RGBA.new(r, g, b, alpha)
|
80
|
+
end
|
81
|
+
|
82
|
+
def to_hsl
|
83
|
+
HSL.new(*hsl_components)
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_hsla(alpha: 1.0)
|
87
|
+
alpha = canonicalize_component(alpha, :alpha)
|
88
|
+
HSLA.new(*hsl_components, alpha)
|
89
|
+
end
|
90
|
+
|
91
|
+
def hsl_components
|
92
|
+
m1, m2 = [r, g, b].minmax
|
93
|
+
c = m2 - m1
|
94
|
+
hh = case
|
95
|
+
when c == 0
|
96
|
+
0r
|
97
|
+
when m2 == r
|
98
|
+
((g - b) / c) % 6r
|
99
|
+
when m2 == g
|
100
|
+
((b - r) / c + 2) % 6r
|
101
|
+
when m2 == b
|
102
|
+
((r - g) / c + 4) % 6r
|
103
|
+
end
|
104
|
+
h = 60r * hh
|
105
|
+
l = 0.5r * m2 + 0.5r * m1
|
106
|
+
s = if l == 1 || l == 0
|
107
|
+
0r
|
108
|
+
else
|
109
|
+
c / (1 - (2*l - 1).abs)
|
110
|
+
end
|
111
|
+
[h, s, l]
|
112
|
+
end
|
113
|
+
|
114
|
+
def to_husl
|
115
|
+
HUSL.from_rgb(r, g, b)
|
116
|
+
end
|
117
|
+
|
118
|
+
def to_xyz
|
119
|
+
XYZ.from_rgb(r, g, b)
|
120
|
+
end
|
121
|
+
|
122
|
+
private def canonicalize(r, g, b)
|
123
|
+
if [r, g, b].map(&:class) == [Integer, Integer, Integer]
|
124
|
+
canonicalize_from_integer(r, g, b)
|
125
|
+
else
|
126
|
+
[
|
127
|
+
canonicalize_component_to_rational(r, :r),
|
128
|
+
canonicalize_component_to_rational(g, :g),
|
129
|
+
canonicalize_component_to_rational(b, :b)
|
130
|
+
]
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
private def canonicalize_from_integer(r, g, b)
|
135
|
+
check_type(r, Integer, :r)
|
136
|
+
check_type(g, Integer, :g)
|
137
|
+
check_type(b, Integer, :b)
|
138
|
+
[
|
139
|
+
canonicalize_component_from_integer(r, :r),
|
140
|
+
canonicalize_component_from_integer(g, :g),
|
141
|
+
canonicalize_component_from_integer(b, :b)
|
142
|
+
]
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
data/lib/colors/rgba.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
module Colors
|
2
|
+
class RGBA < RGB
|
3
|
+
def self.parse(hex_string)
|
4
|
+
case hex_string.to_str.match(/\A#(\h+)\z/) { $1 }.length
|
5
|
+
when 3 # rgb
|
6
|
+
r, g, b = hex_string.scan(/\h/).map {|h| h.hex * 17 }
|
7
|
+
new(r, g, b, 255)
|
8
|
+
when 6 # rrggbb
|
9
|
+
r, g, b = hex_string.scan(/\h{2}/).map(&:hex)
|
10
|
+
new(r, g, b, 255)
|
11
|
+
when 4 # rgba
|
12
|
+
r, g, b, a = hex_string.scan(/\h/).map {|h| h.hex * 17 }
|
13
|
+
new(r, g, b, a)
|
14
|
+
when 8 # rrggbbaa
|
15
|
+
r, g, b, a = hex_string.scan(/\h{2}/).map(&:hex)
|
16
|
+
new(r, g, b, a)
|
17
|
+
else
|
18
|
+
raise ArgumentError, "Invalid hex string: #{hex_string.inspect}"
|
19
|
+
end
|
20
|
+
rescue NoMethodError
|
21
|
+
raise ArgumentError, "hex_string must be a hexadecimal string of `#rrggbb` or `#rgb` form"
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(r, g, b, a)
|
25
|
+
@r, @g, @b, @a = canonicalize(r, g, b, a)
|
26
|
+
end
|
27
|
+
|
28
|
+
include AlphaComponent
|
29
|
+
|
30
|
+
def components
|
31
|
+
[r, g, b, a]
|
32
|
+
end
|
33
|
+
|
34
|
+
def ==(other)
|
35
|
+
case other
|
36
|
+
when RGBA
|
37
|
+
r == other.r && g == other.g && b == other.b && a == other.a
|
38
|
+
when RGB
|
39
|
+
r == other.r && g == other.g && b == other.b && a == 1r
|
40
|
+
else
|
41
|
+
super
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def desaturate(factor)
|
46
|
+
to_hsla.desaturate(factor).to_rgba
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_rgb
|
50
|
+
if a == 1r
|
51
|
+
RGB.new(r, g, b)
|
52
|
+
else
|
53
|
+
raise NotImplementedError,
|
54
|
+
"Unable to convert non-opaque RGBA to RGB"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_rgba
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_hsl
|
63
|
+
if a == 1r
|
64
|
+
super
|
65
|
+
else
|
66
|
+
raise NotImplementedError,
|
67
|
+
"Unable to convert non-opaque RGBA to RGB"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_hsla
|
72
|
+
HSLA.new(*hsl_components, a)
|
73
|
+
end
|
74
|
+
|
75
|
+
private def canonicalize(r, g, b, a)
|
76
|
+
if [r, g, b, a].map(&:class) == [Integer, Integer, Integer, Integer]
|
77
|
+
canonicalize_from_integer(r, g, b, a)
|
78
|
+
else
|
79
|
+
[
|
80
|
+
canonicalize_component_to_rational(r, :r),
|
81
|
+
canonicalize_component_to_rational(g, :g),
|
82
|
+
canonicalize_component_to_rational(b, :b),
|
83
|
+
canonicalize_component_to_rational(a, :a)
|
84
|
+
]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
private def canonicalize_from_integer(r, g, b, a)
|
89
|
+
check_type(r, Integer, :r)
|
90
|
+
check_type(g, Integer, :g)
|
91
|
+
check_type(b, Integer, :b)
|
92
|
+
check_type(a, Integer, :a)
|
93
|
+
[
|
94
|
+
canonicalize_component_from_integer(r, :r),
|
95
|
+
canonicalize_component_from_integer(g, :g),
|
96
|
+
canonicalize_component_from_integer(b, :b),
|
97
|
+
canonicalize_component_from_integer(a, :a)
|
98
|
+
]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/lib/colors/xyz.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
require_relative "helper"
|
2
|
+
|
3
|
+
require "numo/narray"
|
4
|
+
|
5
|
+
module Colors
|
6
|
+
XYZ2RGB = Numo::DFloat[
|
7
|
+
[ 3.24096994190452134377, -1.53738317757009345794, -0.49861076029300328366 ],
|
8
|
+
[ -0.96924363628087982613, 1.87596750150772066772, 0.04155505740717561247 ],
|
9
|
+
[ 0.05563007969699360846, -0.20397695888897656435, 1.05697151424287856072 ]
|
10
|
+
]
|
11
|
+
|
12
|
+
RGB2XYZ = Numo::DFloat[
|
13
|
+
[ 0.41239079926595948129, 0.35758433938387796373, 0.18048078840183428751 ],
|
14
|
+
[ 0.21263900587151035754, 0.71516867876775592746, 0.07219231536073371500 ],
|
15
|
+
[ 0.01933081871559185069, 0.11919477979462598791, 0.95053215224966058086 ]
|
16
|
+
]
|
17
|
+
|
18
|
+
class XYZ < AbstractColor
|
19
|
+
include Helper
|
20
|
+
|
21
|
+
EPSILON = (6/29r)**3
|
22
|
+
|
23
|
+
KAPPA = (29/3)**3
|
24
|
+
|
25
|
+
def self.from_xyY(x, y, large_y)
|
26
|
+
large_x = large_y*x/y
|
27
|
+
large_z = large_y*(1 - x - y)/y
|
28
|
+
new(large_x, large_y, large_z)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.from_rgb(r, g, b)
|
32
|
+
c = RGB2XYZ.dot(Numo::DFloat[to_linear(r), to_linear(g), to_linear(b)])
|
33
|
+
new(c[0], c[1], c[2])
|
34
|
+
end
|
35
|
+
|
36
|
+
private_class_method def self.to_linear(v)
|
37
|
+
if v > 0.04045
|
38
|
+
((v + 0.055r) / 1.055r) ** 2.4r
|
39
|
+
else
|
40
|
+
v / 12.92r
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize(x, y, z)
|
45
|
+
@x, @y, @z = canonicalize(x, y, z)
|
46
|
+
end
|
47
|
+
|
48
|
+
attr_reader :x, :y, :z
|
49
|
+
|
50
|
+
def components
|
51
|
+
[x, y, z]
|
52
|
+
end
|
53
|
+
|
54
|
+
def ==(other)
|
55
|
+
case other
|
56
|
+
when XYZ
|
57
|
+
x == other.x && y == other.y && z == other.z
|
58
|
+
else
|
59
|
+
super
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_rgb
|
64
|
+
RGB.new(*rgb_components)
|
65
|
+
end
|
66
|
+
|
67
|
+
def rgb_components
|
68
|
+
c = XYZ2RGB.dot(Numo::DFloat[x, y, z])
|
69
|
+
[
|
70
|
+
srgb_compand(c[0]).clamp(0r, 1r),
|
71
|
+
srgb_compand(c[1]).clamp(0r, 1r),
|
72
|
+
srgb_compand(c[2]).clamp(0r, 1r)
|
73
|
+
]
|
74
|
+
end
|
75
|
+
|
76
|
+
def luv_components(wp)
|
77
|
+
yy = y/wp.y
|
78
|
+
uu, vv = uv_values
|
79
|
+
l = if yy <= EPSILON
|
80
|
+
KAPPA * yy
|
81
|
+
else
|
82
|
+
116 * Math.cbrt(yy).to_r - 16
|
83
|
+
end
|
84
|
+
if l <= 1e-8
|
85
|
+
u = v = 0r
|
86
|
+
else
|
87
|
+
wp_u, wp_v = wp.uv_values
|
88
|
+
u = 13*l*(uu - wp_u)
|
89
|
+
v = 13*l*(vv - wp_v)
|
90
|
+
end
|
91
|
+
[l, u, v]
|
92
|
+
end
|
93
|
+
|
94
|
+
def uv_values
|
95
|
+
d = x + 15*y + 3*z
|
96
|
+
return [0r, 0r] if d == 0
|
97
|
+
u = 4*x / d
|
98
|
+
v = 9*y / d
|
99
|
+
[u, v]
|
100
|
+
end
|
101
|
+
|
102
|
+
private def srgb_compand(v)
|
103
|
+
# the following is an optimization technique for `1.055*v**(1/2.4) - 0.055`.
|
104
|
+
# x^y ~= exp(y*log(x)) ~= exp2(y*log2(y)); the middle form is faster
|
105
|
+
#
|
106
|
+
# See https://github.com/JuliaGraphics/Colors.jl/issues/351#issuecomment-532073196
|
107
|
+
# for more detail benchmark in Julia language.
|
108
|
+
if v <= 0.0031308
|
109
|
+
12.92*v
|
110
|
+
else
|
111
|
+
1.055 * Math.exp(1/2.4 * Math.log(v)) - 0.055
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
private def canonicalize(x, y, z)
|
116
|
+
[
|
117
|
+
Rational(x),
|
118
|
+
Rational(y),
|
119
|
+
Rational(z)
|
120
|
+
]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
data/red-colors.gemspec
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
clean_white_space = lambda do |entry|
|
2
|
+
entry.gsub(/(\A\n+|\n+\z)/, '') + "\n"
|
3
|
+
end
|
4
|
+
|
5
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "lib"))
|
6
|
+
require "colors/version"
|
7
|
+
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = "red-colors"
|
10
|
+
spec.version = Colors::VERSION
|
11
|
+
spec.homepage = "https://github.com/red-data-tools/red-colors"
|
12
|
+
spec.authors = ["Kenta Murata"]
|
13
|
+
spec.email = ["mrkn@mrkn.jp"]
|
14
|
+
|
15
|
+
readme = File.read("README.md")
|
16
|
+
readme.force_encoding("UTF-8")
|
17
|
+
entries = readme.split(/^\#\#\s(.*)$/)
|
18
|
+
clean_white_space.call(entries[entries.index("Description") + 1])
|
19
|
+
description = clean_white_space.call(entries[entries.index("Description") + 1])
|
20
|
+
spec.summary, spec.description, = description.split(/\n\n+/, 3)
|
21
|
+
spec.license = "MIT"
|
22
|
+
spec.files = [
|
23
|
+
"README.md",
|
24
|
+
"LICENSE.txt",
|
25
|
+
"Rakefile",
|
26
|
+
"Gemfile",
|
27
|
+
"#{spec.name}.gemspec",
|
28
|
+
]
|
29
|
+
spec.files += [".yardopts"]
|
30
|
+
spec.files += Dir.glob("lib/**/*.rb")
|
31
|
+
spec.files += Dir.glob("image/*.*")
|
32
|
+
spec.files += Dir.glob("doc/text/*")
|
33
|
+
spec.test_files += Dir.glob("test/**/*")
|
34
|
+
|
35
|
+
spec.add_development_dependency("bundler")
|
36
|
+
spec.add_development_dependency("rake")
|
37
|
+
spec.add_development_dependency("test-unit")
|
38
|
+
spec.add_development_dependency("yard")
|
39
|
+
spec.add_development_dependency("kramdown")
|
40
|
+
spec.add_development_dependency("numo-narray")
|
41
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
require 'colors'
|
3
|
+
require 'test/unit'
|
4
|
+
|
5
|
+
module TestHelper
|
6
|
+
def assert_near(c1, c2, eps=1e-8)
|
7
|
+
assert_equal(c1.class, c2.class)
|
8
|
+
c1.components.zip(c2.components).each do |x1, x2|
|
9
|
+
x1, x2 = [x1, x2].map(&:to_f)
|
10
|
+
assert { (x1 - x2).abs < eps }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/test/run.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# TODO
|
4
|
+
# $VERBOSE = true
|
5
|
+
|
6
|
+
require "pathname"
|
7
|
+
|
8
|
+
base_dir = Pathname(__dir__).parent.expand_path
|
9
|
+
|
10
|
+
lib_dir = base_dir + "lib"
|
11
|
+
test_dir = base_dir + "test"
|
12
|
+
|
13
|
+
$LOAD_PATH.unshift(lib_dir.to_s)
|
14
|
+
|
15
|
+
require_relative "helper"
|
16
|
+
|
17
|
+
exit(Test::Unit::AutoRunner.run(true, test_dir.to_s))
|