color_calculator 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4095a8dfe79681c7faf97a1610b6fd5f88b8202b4db278543db7dd8c639e1a88
4
+ data.tar.gz: 15cd1be590199c95cbb52d68241cde2a8ba26953aba0832a366e799ed33d359b
5
+ SHA512:
6
+ metadata.gz: f74c83270cf82df3853bc03428069a0760c575a39a48d7e32e1bd2cd1b5ea73bb955892691dfeec1e56900958c3338e9f2d4bb5d244d211852080ff0373cc10b
7
+ data.tar.gz: e4602a3606d6a679512b7f621b116e017e99e59ee93b4e3ce316a60814cb3a095ac5fdb86d1a6fb32b4100146e6fd87b0f9e720ac62910835bb2bb282f0623b7
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ group :development, :test do
8
+ gem 'pry'
9
+ gem 'pry-byebug'
10
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << 'test'
7
+ t.libs << 'lib'
8
+ t.test_files = FileList['test/**/test_*.rb']
9
+ end
10
+
11
+ task :default => :test
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'color_calculator/clump'
4
+ require_relative 'color_calculator/conversion'
5
+ require_relative 'color_calculator/calculation'
6
+
7
+ module ColorCalculator; end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'calculation/delta_e_2000'
4
+
5
+ module ColorCalculator
6
+ module Calculation; end
7
+ end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'color_calculator/clump/lab'
4
+
5
+ module ColorCalculator
6
+ module Calculation
7
+ class DeltaE2000
8
+ class << self
9
+ def call(*args)
10
+ new(*args).call
11
+ end
12
+ end
13
+
14
+ RADIANS_TO_DEGREES = 180.to_f / Math::PI
15
+ DEGREES_TO_RADIANS = 1.to_f / RADIANS_TO_DEGREES
16
+
17
+ K_OF_L = 1
18
+ K_OF_C = 1
19
+ K_OF_H = 1
20
+
21
+ def initialize(sample, reference)
22
+ @sample = sample
23
+ @reference = reference
24
+ end
25
+
26
+ def call
27
+ Math.sqrt(
28
+ [
29
+ Rational(delta_l_prime, K_OF_L * s_of_l) ** 2,
30
+ Rational(delta_c_prime, K_OF_C * s_of_c) ** 2,
31
+ Rational(delta_h_prime, K_OF_H * s_of_h) ** 2,
32
+ [
33
+ r_of_t,
34
+ Rational(delta_c_prime, K_OF_C * s_of_c),
35
+ Rational(delta_h_prime, K_OF_H * s_of_h),
36
+ ].reduce(:*)
37
+ ].reduce(:+)
38
+ )
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :sample, :reference
44
+
45
+ def delta_l_prime
46
+ sample.lightness - reference.lightness
47
+ end
48
+
49
+ def s_of_l
50
+ 1 + Rational(
51
+ 0.015 * ((l_bar_prime - 50) ** 2),
52
+ Math.sqrt(20 + ((l_bar_prime - 50) ** 2))
53
+ )
54
+ end
55
+
56
+ def delta_c_prime
57
+ c_prime_sample - c_prime_reference
58
+ end
59
+
60
+ def s_of_c
61
+ 1 + 0.045 * c_bar_prime
62
+ end
63
+
64
+ def delta_h_prime
65
+ [
66
+ 2,
67
+ Math.sqrt(c_prime_reference * c_prime_sample),
68
+ Math.sin(Rational(delta_little_h_prime, 2) * DEGREES_TO_RADIANS),
69
+ ].reduce(:*)
70
+ end
71
+
72
+ def s_of_h
73
+ 1 + 0.015 * c_bar_prime * big_t
74
+ end
75
+
76
+ def r_of_t
77
+ -1 * Math.sin(2 * delta_theta * DEGREES_TO_RADIANS) * r_of_c
78
+ end
79
+
80
+ %i[reference sample].each do |lab|
81
+ define_method("c_#{lab}") do
82
+ %i[alpha beta].
83
+ map { |val| send(lab).public_send(val) ** 2 }.
84
+ reduce(:+) ** Rational(1, 2)
85
+ end
86
+ end
87
+
88
+ def c_bar
89
+ (c_reference + c_sample) / 2.0
90
+ end
91
+
92
+ def big_g
93
+ 0.5 * (1 - Math.sqrt(Rational(c_bar ** 7, c_bar ** 7 + 25 ** 7)))
94
+ end
95
+
96
+ %i[reference sample].each do |lab|
97
+ # def a_prime_reference
98
+ # def a_prime_sample
99
+ define_method("a_prime_#{lab}") do
100
+ (1 + big_g) * send(lab).alpha
101
+ end
102
+ end
103
+
104
+ %i[reference sample].each do |lab|
105
+ # def c_prime_reference
106
+ # def c_prime_sample
107
+ define_method("c_prime_#{lab}") do
108
+ Math.sqrt(send("a_prime_#{lab}") ** 2 + send(lab).beta ** 2)
109
+ end
110
+ end
111
+
112
+ %i[reference sample].each do |lab|
113
+ # def little_h_prime_reference
114
+ # def little_h_prime_sample
115
+ define_method("little_h_prime_#{lab}") do
116
+ return 0 if [send(lab).beta, send("a_prime_#{lab}")].all? { |el| el == 0 }
117
+
118
+ (Math.atan2(send(lab).beta, send("a_prime_#{lab}")) * RADIANS_TO_DEGREES) % 360
119
+ end
120
+ end
121
+
122
+ def delta_little_h_prime
123
+ c_prime_product = c_prime_reference * c_prime_sample
124
+ difference = little_h_prime_sample - little_h_prime_reference
125
+
126
+ if c_prime_product.zero?
127
+ 0
128
+ elsif !c_prime_product.zero? && difference.abs <= 180
129
+ difference
130
+ elsif !c_prime_product.zero? && difference > 180
131
+ difference - 360
132
+ elsif !c_prime_product.zero? && difference < -180
133
+ difference + 360
134
+ else
135
+ raise "SOMETHING IS WRONG"
136
+ end
137
+ # return 0 if c_prime_sample * c_prime_reference == 0
138
+
139
+ # val = little_h_prime_sample - little_h_prime_reference
140
+
141
+ # return val if val.abs >= 180
142
+ # sign = val <=> 0
143
+
144
+ # val - sign * 360
145
+ end
146
+
147
+ def l_bar_prime
148
+ Rational(reference.lightness + sample.lightness, 2)
149
+ end
150
+
151
+ def c_bar_prime
152
+ Rational(c_prime_reference + c_prime_sample, 2)
153
+ end
154
+
155
+ def little_h_bar_prime
156
+ sum = little_h_prime_reference + little_h_prime_sample
157
+ difference = little_h_prime_reference - little_h_prime_sample
158
+ c_prime_product = c_prime_reference * c_prime_sample
159
+
160
+
161
+ if difference.abs <= 180 && !c_prime_product.zero?
162
+ Rational(sum, 2)
163
+ elsif difference.abs > 180 && sum < 360 && !c_prime_product.zero?
164
+ Rational(sum + 360, 2)
165
+ elsif difference.abs > 180 && sum >= 360 && !c_prime_product.zero?
166
+ Rational(sum - 360, 2)
167
+ elsif c_prime_product.zero?
168
+ sum
169
+ else
170
+ raise "SOMETHING IS WRONG"
171
+ end
172
+ end
173
+
174
+ def big_t
175
+ [
176
+ 1,
177
+ -0.17 * Math.cos((little_h_bar_prime - 30) * DEGREES_TO_RADIANS),
178
+ 0.24 * Math.cos(2 * little_h_bar_prime * DEGREES_TO_RADIANS),
179
+ 0.32 * Math.cos((3 * little_h_bar_prime + 6) * DEGREES_TO_RADIANS),
180
+ -0.2 * Math.cos((4 * little_h_bar_prime - 63) * DEGREES_TO_RADIANS),
181
+ ].reduce(:+)
182
+ end
183
+
184
+ def delta_theta
185
+ 30 * Math::E ** (-1 * (Rational(little_h_bar_prime - 275, 25) ** 2))
186
+ end
187
+
188
+ def r_of_c
189
+ 2 * Math.sqrt(Rational(c_bar_prime ** 7, c_bar_prime ** 7 + 25 ** 7))
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'clump/abstract'
4
+ require_relative 'clump/lab'
5
+ require_relative 'clump/lch_ab'
6
+ require_relative 'clump/rgb'
7
+ require_relative 'clump/xyz'
8
+
9
+ module ColorCalculator
10
+ module Clump; end
11
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ColorCalculator
4
+ module Clump
5
+ class UnlikeComparisonError < StandardError; end
6
+
7
+ class Abstract
8
+ def initialize(*attributes)
9
+ self.class.attributes.each.with_index do |name, index|
10
+ instance_variable_set("@#{name}".to_sym, attributes[index])
11
+
12
+ define_singleton_method(name) do
13
+ instance_variable_get("@#{name}".to_sym)
14
+ end
15
+ end
16
+ end
17
+
18
+ def to_h
19
+ self.class.attributes.reduce({}) do |memo, message|
20
+ memo.merge(message => public_send(message))
21
+ end
22
+ end
23
+
24
+ def ==(other)
25
+ raise UnlikeComparisonError if self.class != other.class
26
+
27
+ to_h == other.to_h
28
+ end
29
+
30
+ def inspect
31
+ to_h.to_s
32
+ end
33
+ alias_method :to_s, :inspect
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'abstract'
4
+
5
+ module ColorCalculator
6
+ module Clump
7
+ class Lab < Abstract
8
+ class << self
9
+ def attributes
10
+ %i[lightness alpha beta]
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'abstract'
4
+
5
+ module ColorCalculator
6
+ module Clump
7
+ class LchAb < Abstract
8
+ class << self
9
+ def attributes
10
+ %i[lightness chroma hue]
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'abstract'
4
+
5
+ module ColorCalculator
6
+ module Clump
7
+ class Rgb < Abstract
8
+ class << self
9
+ def attributes
10
+ %i[red green blue]
11
+ end
12
+ end
13
+
14
+ def initialize(*attributes, normalized:)
15
+ @red, @green, @blue = attributes
16
+ @normalized = normalized
17
+ end
18
+
19
+ %i[red green blue].each do |value|
20
+ define_method(value) do
21
+ Rational(instance_variable_get("@#{value}"), scale)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :normalized
28
+
29
+ def scale
30
+ normalized ? 1 : 255
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'abstract'
4
+
5
+ module ColorCalculator
6
+ module Clump
7
+ class Xyz < Abstract
8
+ class << self
9
+ def attributes
10
+ %i[x y z]
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'conversion/lab_to_lch_ab'
4
+ require_relative 'conversion/rgb_to_xyz'
5
+ require_relative 'conversion/rgb_to_lch_ab'
6
+ require_relative 'conversion/xyz_to_lab'
7
+
8
+ module ColorCalculator
9
+ module Conversion; end
10
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'color_calculator/clump/lab'
4
+ require 'color_calculator/clump/lch_ab'
5
+ require 'color_calculator/shared/composable'
6
+
7
+ module ColorCalculator
8
+ module Conversion
9
+ class LabToLchAb
10
+ extend ColorCalculator::Composable
11
+
12
+ RADIANS_TO_DEGREES = 180 / Math::PI
13
+
14
+ def initialize(lab)
15
+ @lab = lab
16
+ end
17
+
18
+ def call
19
+ ColorCalculator::Clump::LchAb.new(lightness, chroma, hue)
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :lab
25
+
26
+ def lightness
27
+ lab.lightness
28
+ end
29
+
30
+ def chroma
31
+ (lab.alpha ** 2 + lab.beta ** 2) ** Rational(1, 2)
32
+ end
33
+
34
+ def hue
35
+ val = Math.atan2(lab.beta, lab.alpha) * RADIANS_TO_DEGREES
36
+
37
+ return val if val >= 0
38
+
39
+ val + 360
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'color_calculator/shared/composable'
4
+
5
+ module ColorCalculator
6
+ module Conversion
7
+ class RgbToLchAb
8
+ extend ColorCalculator::Composable
9
+
10
+ def initialize(rgb)
11
+ @rgb = rgb
12
+ end
13
+
14
+ def call
15
+ composed_path.call(rgb)
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :rgb
21
+
22
+ def composed_path
23
+ conversion_path.map(&:to_proc).reduce(:>>)
24
+ end
25
+
26
+ def conversion_path
27
+ [
28
+ ColorCalculator::Conversion::RgbToXyz,
29
+ ColorCalculator::Conversion::XyzToLab,
30
+ ColorCalculator::Conversion::LabToLchAb
31
+ ]
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'color_calculator/clump/rgb'
4
+ require 'color_calculator/clump/xyz'
5
+ require 'color_calculator/shared/composable'
6
+
7
+ module ColorCalculator
8
+ module Conversion
9
+ class RgbToXyz
10
+ extend ColorCalculator::Composable
11
+
12
+ def initialize(rgb)
13
+ @rgb = rgb
14
+ end
15
+
16
+ def call
17
+ ColorCalculator::Clump::Xyz.new(x, y, z)
18
+ end
19
+
20
+ APPLE = -> (value) do
21
+ value / 12.92
22
+ end
23
+
24
+ BANANA = -> (value) do
25
+ ((value + 0.055)/1.055) ** Rational(12, 5)
26
+ end
27
+
28
+ FRUIT = -> (value) do
29
+ (value <= 0.04045 ? APPLE : BANANA).call(value)
30
+ end
31
+
32
+ RGB_TO_XYZ_TRANSFORM_D50 = [
33
+ [0.4360747, 0.3850649, 0.1430804],
34
+ [0.2225045, 0.7168786, 0.0606169],
35
+ [0.0139322, 0.0971045, 0.7141733],
36
+ ]
37
+
38
+ private
39
+
40
+ attr_reader :rgb
41
+
42
+ def transformed_rgb
43
+ Hash[
44
+ [:red, :green, :blue].zip(
45
+ RGB_TO_XYZ_TRANSFORM_D50.map do |row|
46
+ %i[red green blue].map.with_index do |rgb, index|
47
+ row[index] * energetically_linear_rgb[rgb]
48
+ end.inject(:+)
49
+ end
50
+ )
51
+ ]
52
+ end
53
+
54
+ def energetically_linear_rgb
55
+ Hash[
56
+ %i[red green blue].map do |color|
57
+ [color, FRUIT.call(rgb.public_send(color))]
58
+ end
59
+ ]
60
+ end
61
+
62
+ (%i[x y z].zip(%i[red green blue])).each do |xyz, rgb|
63
+ define_method(xyz) do
64
+ transformed_rgb[rgb]
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'color_calculator/clump/lab'
4
+ require 'color_calculator/clump/xyz'
5
+ require 'color_calculator/shared/composable'
6
+
7
+ module ColorCalculator
8
+ module Conversion
9
+ class XyzToLab
10
+ extend ColorCalculator::Composable
11
+
12
+ D50 = { x: 0.964220, y: 1.000000, z: 0.825210 }
13
+ EPSILON = 0.008856
14
+ KAPPA = 903.3
15
+
16
+ APPLE = -> (value) do
17
+ value ** Rational(1, 3)
18
+ end
19
+
20
+ BANANA = -> (value) do
21
+ ((KAPPA * value) + 16) / 116
22
+ end
23
+
24
+ FRUIT = -> (value) do
25
+ (value > EPSILON ? APPLE : BANANA).call(value)
26
+ end
27
+
28
+ def initialize(xyz)
29
+ @xyz = xyz
30
+ end
31
+
32
+ def call
33
+ ColorCalculator::Clump::Lab.new(lightness, alpha, beta)
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :xyz
39
+
40
+ def lightness
41
+ (116 * FRUIT.call(y_scaled)) - 16
42
+ end
43
+
44
+ def alpha
45
+ 500 * (FRUIT.call(x_scaled) - FRUIT.call(y_scaled))
46
+ end
47
+
48
+ def beta
49
+ 200 * (FRUIT.call(y_scaled) - FRUIT.call(z_scaled))
50
+ end
51
+
52
+ %i[x y z].each do |symbol|
53
+ define_method("#{symbol}_scaled") do
54
+ xyz.public_send(symbol) / D50.fetch(symbol)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ColorCalculator
4
+ module Composable
5
+ def to_proc
6
+ proc(&method(:call))
7
+ end
8
+
9
+ def call(args)
10
+ new(args).call
11
+ end
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: color_calculator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Nathan Seither
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-03-03 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: You can do color math now!
14
+ email:
15
+ - nathanseither@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - Gemfile
21
+ - Rakefile
22
+ - lib/color_calculator.rb
23
+ - lib/color_calculator/calculation.rb
24
+ - lib/color_calculator/calculation/delta_e_2000.rb
25
+ - lib/color_calculator/clump.rb
26
+ - lib/color_calculator/clump/abstract.rb
27
+ - lib/color_calculator/clump/lab.rb
28
+ - lib/color_calculator/clump/lch_ab.rb
29
+ - lib/color_calculator/clump/rgb.rb
30
+ - lib/color_calculator/clump/xyz.rb
31
+ - lib/color_calculator/conversion.rb
32
+ - lib/color_calculator/conversion/lab_to_lch_ab.rb
33
+ - lib/color_calculator/conversion/rgb_to_lch_ab.rb
34
+ - lib/color_calculator/conversion/rgb_to_xyz.rb
35
+ - lib/color_calculator/conversion/xyz_to_lab.rb
36
+ - lib/color_calculator/shared/composable.rb
37
+ homepage: https://github.com/Supernats/color_calculator
38
+ licenses:
39
+ - MIT
40
+ metadata: {}
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 2.6.0
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubygems_version: 3.0.3
57
+ signing_key:
58
+ specification_version: 4
59
+ summary: Color math calculators and converters
60
+ test_files: []