color_calculator 0.0.1

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.
@@ -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: []