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.
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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Colors
2
+ VERSION = "0.1.0"
3
+ 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
@@ -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))