red-colors 0.1.0

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