color 1.8 → 2.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.
Files changed (53) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +314 -0
  3. data/CODE_OF_CONDUCT.md +128 -0
  4. data/CONTRIBUTING.md +84 -0
  5. data/CONTRIBUTORS.md +11 -0
  6. data/LICENCE.md +50 -0
  7. data/Manifest.txt +12 -23
  8. data/README.md +54 -0
  9. data/Rakefile +72 -61
  10. data/SECURITY.md +39 -0
  11. data/lib/color/cielab.rb +348 -0
  12. data/lib/color/cmyk.rb +279 -213
  13. data/lib/color/grayscale.rb +128 -160
  14. data/lib/color/hsl.rb +205 -173
  15. data/lib/color/rgb/colors.rb +211 -161
  16. data/lib/color/rgb.rb +527 -551
  17. data/lib/color/version.rb +5 -0
  18. data/lib/color/xyz.rb +214 -0
  19. data/lib/color/yiq.rb +91 -46
  20. data/lib/color.rb +204 -142
  21. data/licences/dco.txt +34 -0
  22. data/test/fixtures/cielab.json +444 -0
  23. data/test/minitest_helper.rb +20 -4
  24. data/test/test_cmyk.rb +49 -72
  25. data/test/test_color.rb +58 -112
  26. data/test/test_grayscale.rb +35 -57
  27. data/test/test_hsl.rb +71 -77
  28. data/test/test_rgb.rb +195 -267
  29. data/test/test_yiq.rb +12 -30
  30. metadata +104 -121
  31. data/.autotest +0 -5
  32. data/.coveralls.yml +0 -2
  33. data/.gemtest +0 -0
  34. data/.hoerc +0 -2
  35. data/.minitest.rb +0 -2
  36. data/.travis.yml +0 -41
  37. data/Code-of-Conduct.rdoc +0 -41
  38. data/Contributing.rdoc +0 -62
  39. data/Gemfile +0 -9
  40. data/History.rdoc +0 -194
  41. data/Licence.rdoc +0 -27
  42. data/README.rdoc +0 -52
  43. data/lib/color/css.rb +0 -7
  44. data/lib/color/palette/adobecolor.rb +0 -260
  45. data/lib/color/palette/gimp.rb +0 -104
  46. data/lib/color/palette/monocontrast.rb +0 -164
  47. data/lib/color/palette.rb +0 -4
  48. data/lib/color/rgb/contrast.rb +0 -57
  49. data/lib/color/rgb/metallic.rb +0 -28
  50. data/test/test_adobecolor.rb +0 -405
  51. data/test/test_css.rb +0 -19
  52. data/test/test_gimp.rb +0 -87
  53. data/test/test_monocontrast.rb +0 -130
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # Color -- Color Math in Ruby
2
+
3
+ - code :: <https://github.com/halostatue/color>
4
+ - issues :: <https://github.com/halostatue/color/issues>
5
+ - changelog :: <https://github.com/halostatue/color/blob/main/CHANGELOG.md>
6
+ - continuous integration ::
7
+ [![Build Status](https://github.com/halostatue/color/actions/workflows/ci.yml/badge.svg)][ci-workflow]
8
+ - test coverage ::
9
+ [![Coverage](https://coveralls.io/repos/halostatue/color/badge.svg?branch=main&service=github)][coveralls]
10
+
11
+ ## Description
12
+
13
+ Color is a Ruby library to provide RGB, CMYK, HSL, and other color space
14
+ manipulation support to applications that require it. It provides optional named
15
+ RGB colors that are commonly supported in HTML, SVG, and X11 applications.
16
+
17
+ The Color library performs purely mathematical manipulation of the colors based
18
+ on color theory without reference to device color profiles (such as sRGB or
19
+ Adobe RGB). For most purposes, when working with RGB and HSL color spaces, this
20
+ won't matter. Absolute color spaces (like CIE LAB and CIE XYZ) cannot be
21
+ reliably converted to relative color spaces (like RGB) without color profiles.
22
+ When necessary for conversions, Color provides D65 and D50 reference white
23
+ values in Color::XYZ.
24
+
25
+ Color 2.0 is a major release, dropping support for all versions of Ruby prior to
26
+ 3.2 as well as removing or renaming a number of features. The main breaking
27
+ changes are:
28
+
29
+ - Color classes are immutable Data objects; they are no longer mutable.
30
+ - RGB named colors are no longer loaded on gem startup, but must be required
31
+ explicitly (this is _not_ done via `autoload` because there are more than 100
32
+ named colors with spelling variations) with `require "color/rgb/colors"`.
33
+ - Color palettes have been removed.
34
+ - `Color::CSS` and `Color::CSS#[]` have been removed.
35
+
36
+ ## Example
37
+
38
+ Suppose you want to make a given RGB color a little lighter. Adjusting the RGB
39
+ color curves will change the hue and saturation will also change. Instead, use
40
+ the CIE LAB color space keeping the `color` components intact, altering only the
41
+ lightness component:
42
+
43
+ ```ruby
44
+ c = Color::RGB.from_values(r, g, b).to_lab
45
+
46
+ if (t = c.l / 50.0) < 1
47
+ c.l = 50 * ((1.0 - t) * Math.sqrt(t) + t**2)
48
+ end
49
+
50
+ c.to_rgb
51
+ ```
52
+
53
+ [ci-workflow]: https://github.com/halostatue/color/actions/workflows/ci.yml
54
+ [coveralls]: https://coveralls.io/github/halostatue/color?branch=main
data/Rakefile CHANGED
@@ -1,75 +1,86 @@
1
- # -*- ruby -*-
1
+ require "rubygems"
2
+ require "hoe"
3
+ require "rake/clean"
4
+ require "rdoc/task"
5
+ require "minitest"
6
+ require "minitest/test_task"
2
7
 
3
- require 'rubygems'
4
- require 'hoe'
5
- require 'rake/clean'
8
+ Hoe.plugin :halostatue
9
+ Hoe.plugin :rubygems
6
10
 
7
- Hoe.plugin :doofus
8
- Hoe.plugin :gemspec2
9
- Hoe.plugin :git
10
- Hoe.plugin :minitest
11
- Hoe.plugin :travis
12
- Hoe.plugin :email unless ENV['CI'] or ENV['TRAVIS']
11
+ Hoe.plugins.delete :debug
12
+ Hoe.plugins.delete :newb
13
+ Hoe.plugins.delete :publish
14
+ Hoe.plugins.delete :signing
15
+ Hoe.plugins.delete :test
13
16
 
14
- spec = Hoe.spec 'color' do
15
- developer('Austin Ziegler', 'halostatue@gmail.com')
16
- developer('Matt Lyon', 'matt@postsomnia.com')
17
+ hoe = Hoe.spec "color" do
18
+ developer("Austin Ziegler", "halostatue@gmail.com")
19
+ developer("Matt Lyon", "matt@postsomnia.com")
17
20
 
18
- license 'MIT'
21
+ self.trusted_release = ENV["rubygems_release_gem"] == "true"
19
22
 
20
- self.history_file = 'History.rdoc'
21
- self.readme_file = 'README.rdoc'
23
+ require_ruby_version ">= 3.2"
22
24
 
23
- extra_dev_deps << ['hoe-doofus', '~> 1.0']
24
- extra_dev_deps << ['hoe-gemspec2', '~> 1.1']
25
- extra_dev_deps << ['hoe-git', '~> 1.6']
26
- extra_dev_deps << ['hoe-travis', '~> 1.2']
27
- extra_dev_deps << ['minitest', '~> 5.8']
28
- extra_dev_deps << ['minitest-around', '~> 0.3']
29
- extra_dev_deps << ['minitest-autotest', '~> 1.0']
30
- extra_dev_deps << ['minitest-bisect', '~> 1.2']
31
- extra_dev_deps << ['minitest-focus', '~> 1.1']
32
- extra_dev_deps << ['minitest-moar', '~> 0.0']
33
- extra_dev_deps << ['minitest-pretty_diff', '~> 0.1']
34
- extra_dev_deps << ['rake', '~> 10.0']
25
+ license "MIT"
35
26
 
36
- if RUBY_VERSION >= '1.9'
37
- extra_dev_deps << ['simplecov', '~> 0.7']
38
- extra_dev_deps << ['coveralls', '~> 0.7'] if ENV['CI'] or ENV['TRAVIS']
39
- end
40
- end
27
+ spec_extras[:metadata] = ->(val) {
28
+ val["rubygems_mfa_required"] = "true"
29
+ }
41
30
 
42
- if RUBY_VERSION >= '1.9'
43
- namespace :test do
44
- if ENV['CI'] or ENV['TRAVIS']
45
- desc "Submit test coverage to Coveralls"
46
- task :coveralls do
47
- spec.test_prelude = [
48
- 'require "psych"',
49
- 'require "simplecov"',
50
- 'require "coveralls"',
51
- 'SimpleCov.formatter = Coveralls::SimpleCov::Formatter',
52
- 'SimpleCov.start("test_frameworks") { command_name "Minitest" }',
53
- 'gem "minitest"'
54
- ].join('; ')
55
- Rake::Task['test'].execute
56
- end
31
+ extra_dev_deps << ["hoe", "~> 4.0"]
32
+ extra_dev_deps << ["hoe-halostatue", "~> 2.1", ">= 2.1.1"]
33
+ extra_dev_deps << ["hoe-git", "~> 1.6"]
34
+ extra_dev_deps << ["minitest", "~> 5.8"]
35
+ extra_dev_deps << ["minitest-autotest", "~> 1.0"]
36
+ extra_dev_deps << ["minitest-focus", "~> 1.1"]
37
+ extra_dev_deps << ["minitest-moar", "~> 0.0"]
38
+ extra_dev_deps << ["rake", ">= 10.0", "< 14"]
39
+ extra_dev_deps << ["rdoc", ">= 0.0", "< 7"]
40
+ extra_dev_deps << ["standard", "~> 1.0"]
41
+ extra_dev_deps << ["json", ">= 0.0"]
42
+ extra_dev_deps << ["simplecov", "~> 0.22"]
43
+ extra_dev_deps << ["simplecov-lcov", "~> 0.8"]
44
+ end
57
45
 
58
- Rake::Task['travis'].prerequisites.replace(%w(test:coveralls))
59
- end
46
+ Minitest::TestTask.create :test
47
+ Minitest::TestTask.create :coverage do |t|
48
+ formatters = <<-RUBY.split($/).join(" ")
49
+ SimpleCov::Formatter::MultiFormatter.new([
50
+ SimpleCov::Formatter::HTMLFormatter,
51
+ SimpleCov::Formatter::LcovFormatter,
52
+ SimpleCov::Formatter::SimpleFormatter
53
+ ])
54
+ RUBY
55
+ t.test_prelude = <<-RUBY.split($/).join("; ")
56
+ require "simplecov"
57
+ require "simplecov-lcov"
60
58
 
61
- desc "Runs test coverage. Only works Ruby 1.9+ and assumes 'simplecov' is installed."
62
- task :coverage do
63
- spec.test_prelude = [
64
- 'require "simplecov"',
65
- 'SimpleCov.start("test_frameworks") { command_name "Minitest" }',
66
- 'gem "minitest"'
67
- ].join('; ')
68
- Rake::Task['test'].execute
69
- end
59
+ SimpleCov::Formatter::LcovFormatter.config do |config|
60
+ config.report_with_single_file = true
61
+ config.lcov_file_name = "lcov.info"
62
+ end
70
63
 
71
- CLOBBER << 'coverage'
64
+ SimpleCov.start "test_frameworks" do
65
+ enable_coverage :branch
66
+ primary_coverage :branch
67
+ formatter #{formatters}
72
68
  end
69
+ RUBY
73
70
  end
74
71
 
75
- # vim: syntax=ruby
72
+ task default: :test
73
+
74
+ task :version do
75
+ require "color/version"
76
+ puts Color::VERSION
77
+ end
78
+
79
+ RDoc::Task.new do
80
+ _1.title = "Color -- Color Math with Ruby"
81
+ _1.main = "lib/color.rb"
82
+ _1.rdoc_dir = "doc"
83
+ _1.rdoc_files = hoe.spec.require_paths - ["Manifest.txt"] + hoe.spec.extra_rdoc_files
84
+ _1.markup = "markdown"
85
+ end
86
+ task docs: :rerdoc
data/SECURITY.md ADDED
@@ -0,0 +1,39 @@
1
+ # color Security
2
+
3
+ ## LLM-Generated Security Report Policy
4
+
5
+ Absolutely no security reports will be accepted that have been generated by LLM
6
+ agents.
7
+
8
+ ## Supported Versions
9
+
10
+ Security reports are accepted for the most recent major release and the previous
11
+ version for a limited time after the initial major release version. After a
12
+ major release, the previous version will receive full support for three months
13
+ and security support for an additional three months (for a total of six months).
14
+
15
+ Because color 1.x supports a wide range of Ruby versions that are themselves end
16
+ of life, security reports will only be accepted when they can be demonstrated on
17
+ Ruby 3.2 or higher.
18
+
19
+ > | Version | Release Date | Support Ends | Security Support Ends |
20
+ > | ------- | ------------ | -------------- | --------------------- |
21
+ > | 1.x | 2015-10-26 | 2.x + 3 months | 2.x + 6 months |
22
+ > | 2.x | 2025-MM-DD | - | - |
23
+
24
+ ## Reporting a Vulnerability
25
+
26
+ By preference, use the [Tidelift security contact][tidelift]. Tidelift will
27
+ coordinate the fix and disclosure.
28
+
29
+ Alternatively, Send an email to [color@halostatue.ca][email] with the text
30
+ `Color` in the subject. Emails sent to this address should be encrypted using
31
+ [age][age] with the following public key:
32
+
33
+ ```
34
+ age1fc6ngxmn02m62fej5cl30lrvwmxn4k3q2atqu53aatekmnqfwumqj4g93w
35
+ ```
36
+
37
+ [tidelift]: https://tidelift.com/security
38
+ [email]: mailto:color@halostatue.ca
39
+ [age]: https://github.com/FiloSottile/age
@@ -0,0 +1,348 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # A \Color object for the \CIELAB color space (also known as L\*a\*\b*). Color is
5
+ # expressed in a three-dimensional, device-independent "standard observer" model, often
6
+ # in relation to a "reference white" color, usually Color::XYZ::D65 (most purposes) or
7
+ # Color::XYZ::D50 (printing).
8
+ #
9
+ # `L*` is the perceptual lightness, bounded to values between 0 (black) and 100 (white).
10
+ # `a*` is the range of green (negative) / red (positive) and `b*` is the range of blue
11
+ # (negative) / yellow (positive).
12
+ #
13
+ # The `a*` and `b*` ranges are _technically_ unbounded but \Color clamps them to the
14
+ # values `-128..127`.
15
+ #
16
+ # For more information, see [CIELAB](https://en.wikipedia.org/wiki/CIELAB_color_space).
17
+ #
18
+ # \CIELAB colors are immutable Data class instances. Array deconstruction is `[l, a, b]`
19
+ # and hash deconstruction is `{l:, a:, b:}` (see #l, #a, #b).
20
+ class Color::CIELAB
21
+ include Color
22
+
23
+ ##
24
+ # Standard weights applied for perceptual differences using the ΔE*94 algorithm.
25
+ DE94_WEIGHTS = {
26
+ graphic_arts: {k_1: 0.045, k_2: 0.015, k_l: 1.0}.freeze,
27
+ textiles: {k_1: 0.048, k_2: 0.014, k_l: 2.0}.freeze
28
+ }.freeze
29
+
30
+ RANGES = {L: 0.0..100.0, ab: -128.0..127.0}.freeze # :nodoc:
31
+ private_constant :RANGES
32
+
33
+ ##
34
+ # :attr_reader: l
35
+ # The `L*` attribute of this \CIELAB color object expressed as a value 0..100.
36
+
37
+ ##
38
+ # :attr_reader: a
39
+ # The `a*` attribute of this \CIELAB color object expressed as a value -128..127.
40
+
41
+ ##
42
+ # :attr_reader: b
43
+ # The `b*` attribute of this \CIELAB color object expressed as a value -128..127.
44
+
45
+ ##
46
+ # Creates a \CIELAB color representation from percentage values.
47
+ #
48
+ # `l` must be between 0% and 100%; `a` and `b` must be between -100% and 100% and will
49
+ # be transposed to the native value -128..127.
50
+ #
51
+ # ```ruby
52
+ # Color::CIELAB.from_percentage(10, -30, 30) # => CIELAB [10.0000 -38.7500 37.7500]
53
+ # ```
54
+ #
55
+ # :call-seq:
56
+ # from_percentage(l, a, b)
57
+ # from_percentage(l:, a:, b:)
58
+ def self.from_percentage(*args, **kwargs)
59
+ l, a, b =
60
+ case [args, kwargs]
61
+ in [[_, _, _], {}]
62
+ args
63
+ in [[], {l:, a:, b:}]
64
+ [l, a, b]
65
+ else
66
+ new(*args, **kwargs)
67
+ end
68
+
69
+ new(
70
+ l: l,
71
+ a: Color.translate_range(a, from: -100.0..100.0, to: RANGES[:ab]),
72
+ b: Color.translate_range(b, from: -100.0..100.0, to: RANGES[:ab])
73
+ )
74
+ end
75
+
76
+ class << self
77
+ alias_method :from_values, :new
78
+ alias_method :from_internal, :new # :nodoc:
79
+ end
80
+
81
+ ##
82
+ # Creates a \CIELAB color representation from `L*a*b*` native values. The `l` value
83
+ # must be between 0 and 100 and the `a` and `b` values must be between -128 and 127.
84
+ #
85
+ # ```ruby
86
+ # Color::CIELAB.new(10, 35, -35) # => CIELAB [10.00 35.00 -35.00]
87
+ # Color::CIELAB.from_values(10, 35, -35) # => CIELAB [10.00 35.00 -35.00]
88
+ # Color::CIELAB[l: 10, a: 35, b: -35] # => CIELAB [10.00 35.00 -35.00]
89
+ # ```
90
+ def initialize(l:, a:, b:)
91
+ super(
92
+ l: normalize(l, RANGES[:L]),
93
+ a: normalize(a, RANGES[:ab]),
94
+ b: normalize(b, RANGES[:ab])
95
+ )
96
+ end
97
+
98
+ ##
99
+ # Coerces the other Color object into \CIELAB.
100
+ def coerce(other) = other.to_lab
101
+
102
+ ##
103
+ # Converts \CIELAB to Color::CMYK via Color::RGB.
104
+ #
105
+ # See #to_rgb and Color::RGB#to_cmyk.
106
+ def to_cmyk(...) = to_rgb(...).to_cmyk(...)
107
+
108
+ ##
109
+ # Converts \CIELAB to Color::Grayscale via Color::RGB.
110
+ #
111
+ # See #to_rgb and Color::RGB#to_grayscale.
112
+ def to_grayscale(...) = to_rgb(...).to_grayscale(...)
113
+
114
+ ##
115
+ def to_lab(...) = self
116
+
117
+ ##
118
+ # Converts \CIELAB to Color::HSL via Color::RGB.
119
+ #
120
+ # See #to_rgb and Color::RGB#to_hsl.
121
+ def to_hsl(...) = to_rgb(...).to_hsl(...)
122
+
123
+ ##
124
+ # Converts \CIELAB to Color::RGB via Color::XYZ.
125
+ #
126
+ # See #to_xyz and Color::XYZ#to_rgb.
127
+ def to_rgb(...) = to_xyz(...).to_rgb(...)
128
+
129
+ ##
130
+ # Converts \CIELAB to Color::XYZ based on a reference white.
131
+ #
132
+ # Accepts a single keyword parameter, `white`, indicating the reference white used for
133
+ # conversion scaling. If none is provided, Color::XYZ::D65 is used.
134
+ #
135
+ # :call-seq:
136
+ # to_xyz(white: Color::XYZ::D65)
137
+ def to_xyz(*args, **kwargs)
138
+ fy = (l + 16.0) / 116
139
+ fz = fy - b / 200.0
140
+ fx = a / 500.0 + fy
141
+
142
+ xr = ((fx3 = fx**3) > Color::XYZ::E) ? fx3 : (116.0 * fx - 16) / Color::XYZ::K
143
+ yr = (l > Color::XYZ::EK) ? ((l + 16.0) / 116)**3 : l
144
+ zr = ((fz3 = fz**3) > Color::XYZ::E) ? fz3 : (116.0 * fz - 16) / Color::XYZ::K
145
+
146
+ ref = kwargs[:white] || args.first
147
+ ref = Color::XYZ::D65 unless ref.is_a?(Color::XYZ)
148
+
149
+ ref.scale(xr, yr, zr)
150
+ end
151
+
152
+ ##
153
+ # Render the CSS `lab()` function for this \CIELAB object, adding an `alpha` if
154
+ # provided.
155
+ def css(alpha: nil, **)
156
+ params = [css_value(l, :percent), css_value(a), css_value(b)].join(" ")
157
+ params = "#{params} / #{css_value(alpha)}" if alpha
158
+
159
+ "lab(#{params})"
160
+ end
161
+
162
+ ##
163
+ # Implements the \CIELAB ΔE* 2000 perceptual color distance metric with more reliable
164
+ # results over \CIELAB ΔE* 1994.
165
+ #
166
+ # See [CIEDE2000][ciede2000] for precise details on the mathematical formulas. The
167
+ # implementation here is based on Sharma, Wu, and Dala in [CIEDE2000.xls][ciede2000xls],
168
+ # published as supplementary materials for their paper "The CIEDE2000 Color-Difference
169
+ # Formula: Implementation Notes, Supplementary Test Data, and Mathematical
170
+ # Observations,", G. Sharma, W. Wu, E. N. Dalal, Color Research and Application, vol.
171
+ # 30. No. 1, pp. 21-30, February 2005.
172
+ #
173
+ # Do not override the `klch` parameter unless you _really_ know what you're doing.
174
+ #
175
+ # See also <http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE2000.html>
176
+ #
177
+ # [ciede2000]: https://en.wikipedia.org/wiki/Color_difference#CIEDE2000
178
+ # [ciede2000xls]: http://www.ece.rochester.edu/~gsharma/ciede2000/dataNprograms/CIEDE2000.xls
179
+ def delta_e2000(other, klch: {L: 1.0, C: 1.0, H: 1.0})
180
+ other = coerce(other)
181
+ klch => L: k_l, C: k_c, H: k_h
182
+ self => l: l_star_1, a: a_star_1, b: b_star_1
183
+ other => l: l_star_2, a: a_star_2, b: b_star_2
184
+
185
+ v_25_pow_7 = 25**7
186
+
187
+ c_star_1 = Math.sqrt(a_star_1**2 + b_star_1**2)
188
+ c_star_2 = Math.sqrt(a_star_2**2 + b_star_2**2)
189
+
190
+ c_mean = ((c_star_1 + c_star_2) / 2.0)
191
+ c_mean_pow_7 = c_mean**7
192
+ c_mean_g = (0.5 * (1.0 - Math.sqrt(c_mean_pow_7 / (c_mean_pow_7 + v_25_pow_7))))
193
+
194
+ a_1_prime = ((1.0 + c_mean_g) * a_star_1)
195
+ a_2_prime = ((1.0 + c_mean_g) * a_star_2)
196
+
197
+ c_1_prime = Math.sqrt(a_1_prime**2 + b_star_1**2)
198
+ c_2_prime = Math.sqrt(a_2_prime**2 + b_star_2**2)
199
+
200
+ h_1_prime =
201
+ if a_1_prime + b_star_1 == 0
202
+ 0
203
+ else
204
+ (to_degrees(Math.atan2(b_star_1, a_1_prime)) % 360.0)
205
+ end
206
+ h_2_prime =
207
+ if a_2_prime + b_star_2 == 0
208
+ 0
209
+ else
210
+ (to_degrees(Math.atan2(b_star_2, a_2_prime)) % 360.0)
211
+ end
212
+
213
+ delta_lower_h_prime =
214
+ if h_2_prime - h_1_prime < -180
215
+ h_2_prime + 360 - h_1_prime
216
+ elsif h_2_prime - h_1_prime > 180
217
+ h_2_prime - h_1_prime - 360.0
218
+ else
219
+ h_2_prime - h_1_prime
220
+ end
221
+
222
+ delta_upper_l_prime = l_star_2 - l_star_1
223
+ delta_upper_c_prime = c_2_prime - c_1_prime
224
+ delta_upper_h_prime = (
225
+ 2.0 *
226
+ Math.sqrt(c_1_prime * c_2_prime) *
227
+ Math.sin(to_radians(delta_lower_h_prime / 2.0))
228
+ )
229
+
230
+ l_prime_mean = ((l_star_1 + l_star_2) / 2.0)
231
+ c_prime_mean = ((c_1_prime + c_2_prime) / 2.0)
232
+ h_prime_mean =
233
+ if c_1_prime * c_2_prime == 0
234
+ h_1_prime + h_2_prime
235
+ elsif (h_2_prime - h_1_prime).abs <= 180
236
+ ((h_1_prime + h_2_prime) / 2.0)
237
+ elsif h_2_prime + h_1_prime <= 360
238
+ ((h_1_prime + h_2_prime) / 2.0 + 180.0)
239
+ else
240
+ ((h_1_prime + h_2_prime) / 2.0 - 180.0)
241
+ end
242
+
243
+ l_prime_mean50sq = ((l_prime_mean - 50)**2)
244
+
245
+ upper_s_l = (1 + (0.015 * l_prime_mean50sq / Math.sqrt(20 + l_prime_mean50sq)))
246
+ upper_s_c = (1 + 0.045 * c_prime_mean)
247
+ upper_t = (
248
+ 1 -
249
+ 0.17 * Math.cos(to_radians(h_prime_mean - 30)) +
250
+ 0.24 * Math.cos(to_radians(2 * h_prime_mean)) +
251
+ 0.32 * Math.cos(to_radians(3 * h_prime_mean + 6)) -
252
+ 0.2 * Math.cos(to_radians(4 * h_prime_mean - 63))
253
+ )
254
+
255
+ upper_s_h = (1 + 0.015 * c_prime_mean * upper_t)
256
+
257
+ delta_theta = (30 * Math.exp(-1 * ((h_prime_mean - 275) / 25.0)**2))
258
+ upper_r_c = (2 * Math.sqrt(c_prime_mean**7 / (c_prime_mean**7 + v_25_pow_7)))
259
+ upper_r_t = (-Math.sin(to_radians(2 * delta_theta)) * upper_r_c)
260
+ delta_l_prime_div_kl_div_sl = (delta_upper_l_prime / upper_s_l / k_l.to_f)
261
+ delta_c_prime_div_kc_div_sc = (delta_upper_c_prime / upper_s_c / k_c.to_f)
262
+ delta_h_prime_div_kh_div_sh = (delta_upper_h_prime / upper_s_h / k_h.to_f)
263
+
264
+ Math.sqrt(
265
+ delta_l_prime_div_kl_div_sl**2 +
266
+ delta_c_prime_div_kc_div_sc**2 +
267
+ delta_h_prime_div_kh_div_sh**2 +
268
+ upper_r_t * delta_c_prime_div_kc_div_sc * delta_h_prime_div_kh_div_sh
269
+ )
270
+ end
271
+
272
+ ##
273
+ # Implements the \CIELAB ΔE* 1994 perceptual color distance metric. This version is an
274
+ # improvement over previous versions, but it does not handle perceptual discontinuities
275
+ # as well as \CIELAB ΔE* 2000. This is implemented because some functions still require
276
+ # the 1994 algorithm for proper operation.
277
+ #
278
+ # See [CIE94][cie94] for precise details on the mathematical formulas.
279
+ #
280
+ # Different weights for `k_l`, `k_1`, and `k_2` may be applied via the `weight` keyword
281
+ # parameter. This may be provided either as a Hash with `k_l`, `k_1`, and `k_2` values
282
+ # or as a key to DE94_WEIGHTS. The default weight is `:graphic_arts`.
283
+ #
284
+ # See also <http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html>.
285
+ #
286
+ # [cie94]: https://en.wikipedia.org/wiki/Color_difference#CIE94
287
+ def delta_e94(other, weight: :graphic_arts)
288
+ weight = DE94_WEIGHTS[weight] if DE94_WEIGHTS.key?(weight)
289
+ raise ArgumentError, "Unsupported weight #{weight.inspect}." unless weight.is_a?(Hash)
290
+
291
+ weight => k_1:, k_2:, k_l:
292
+
293
+ # Under some circumstances in real computers, the computed value of ΔH could be an
294
+ # imaginary number (it's a square root value), so instead of √(((ΔL/(kL*sL))²) +
295
+ # ((ΔC/(kC*sC))²) + ((ΔH/(kH*sH))²)), we have implemented the final computation as
296
+ # √(((ΔL/(kL*sL))²) + ((ΔC/(kC*sC))²) + (ΔH2/(kH*sH)²)) and not performing the square
297
+ # root when computing ΔH2.
298
+
299
+ k_c = k_h = 1.0
300
+
301
+ other = coerce(other)
302
+
303
+ self => l: l_1, a: a_1, b: b_1
304
+ other => l: l_2, a: a_2, b: b_2
305
+
306
+ delta_a = a_1 - a_2
307
+ delta_b = b_1 - b_2
308
+
309
+ cab_1 = Math.sqrt((a_1**2) + (b_1**2))
310
+ cab_2 = Math.sqrt((a_2**2) + (b_2**2))
311
+
312
+ delta_upper_l = l_1 - l_2
313
+ delta_upper_c = cab_1 - cab_2
314
+
315
+ delta_h2 = (delta_a**2) + (delta_b**2) - (delta_upper_c**2)
316
+
317
+ s_upper_l = 1.0
318
+ s_upper_c = 1 + k_1 * cab_1
319
+ s_upper_h = 1 + k_2 * cab_1
320
+
321
+ composite_upper_l = (delta_upper_l / (k_l * s_upper_l))**2
322
+ composite_upper_c = (delta_upper_c / (k_c * s_upper_c))**2
323
+ composite_upper_h = delta_h2 / ((k_h * s_upper_h)**2)
324
+ Math.sqrt(composite_upper_l + composite_upper_c + composite_upper_h)
325
+ end
326
+
327
+ ##
328
+ alias_method :to_a, :deconstruct
329
+
330
+ ##
331
+ alias_method :to_internal, :deconstruct # :nodoc:
332
+
333
+ ##
334
+ def inspect = "CIELAB [%.4f %.4f %.4f]" % [l, a, b] # :nodoc:
335
+
336
+ ##
337
+ def pretty_print(q) # :nodoc:
338
+ q.text "CIELAB"
339
+ q.breakable
340
+ q.group 2, "[", "]" do
341
+ q.text "%.4f" % l
342
+ q.fill_breakable
343
+ q.text "%.4f" % a
344
+ q.fill_breakable
345
+ q.text "%.4f" % b
346
+ end
347
+ end
348
+ end