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