File: color.rb

package info (click to toggle)
ruby-color 2.1.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 300 kB
  • sloc: ruby: 1,801; makefile: 5
file content (249 lines) | stat: -rw-r--r-- 8,156 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# frozen_string_literal: true

# # \Color -- \Color Math in Ruby
#
# - **code**: [github.com/halostatue/color](https://github.com/halostatue/color)
# - **issues**: [github.com/halostatue/color/issues](https://github.com/halostatue/color/issues)
# - **changelog**: [CHANGELOG](rdoc-ref:CHANGELOG.md)
#
# \Color is a Ruby library to provide RGB, CMYK, HSL, and other color space manipulation
# support to applications that require it. It provides optional named RGB colors that are
# commonly supported in HTML, SVG, and X11 applications.
#
# The \Color library performs purely mathematical manipulation of the colors based on
# color theory without reference to device color profiles (such as sRGB or Adobe RGB). For
# most purposes, when working with RGB and HSL color spaces, this won't matter. Absolute
# color spaces (like CIE LAB and CIE XYZ) cannot be reliably converted to relative color
# spaces (like RGB) without color profiles. When necessary for conversions, \Color
# provides \D65 and \D50 reference white values in Color::XYZ.
#
# Color 2.1 fixes a Color::XYZ bug where the values were improperly clamped and adds more
# Color::XYZ white points for standard illuminants. It builds on the Color 2.0 major
# release, dropping support for all versions of Ruby prior to 3.2 as well as removing or
# renaming a number of features. The main breaking changes are:
#
# - Color classes are immutable Data objects; they are no longer mutable.
# - RGB named colors are no longer loaded on gem startup, but must be required explicitly
#   (this is _not_ done via `autoload` because there are more than 100 named colors with
#   spelling variations) with `require "color/rgb/colors"`.
# - Color palettes have been removed.
# - `Color::CSS` and `Color::CSS#[]` have been removed.
module Color
  ##
  # The maximum "resolution" for color math; if any value is less than or equal to this
  # value, it is treated as zero.
  EPSILON = 1e-5

  ##
  # The tolerance for comparing the components of two colors. In general, colors are
  # considered equal if all of their components are within this tolerance value of each
  # other.
  TOLERANCE = 1e-4

  # :stopdoc:
  CIELAB = Data.define(:l, :a, :b)
  CMYK = Data.define(:c, :m, :y, :k)
  Grayscale = Data.define(:g)
  HSL = Data.define(:h, :s, :l)
  RGB = Data.define(:r, :g, :b, :names)
  XYZ = Data.define(:x, :y, :z)
  YIQ = Data.define(:y, :i, :q)
  # :startdoc:

  ##
  # It is useful to know the number of components in some cases. Since most colors are
  # defined with three components, we define a constant value here. Color classes that
  # require more or less should override this.
  #
  # We _could_ define this as `members.count`, but this would require a special case
  # for Color::RGB _regardless_ because there's an additional member for RGB colors
  # (names).
  def components = 3 # :nodoc:

  ##
  # Compares the `other` color to this one. The `other` color will be coerced to the same
  # type as the current color. Such converted color comparisons will always be more
  # approximate than non-converted comparisons.
  #
  # All values are compared as floating-point values, so two colors will be reported
  # equivalent if all component values are within +TOLERANCE+ of each other.
  def ==(other)
    other.is_a?(Color) && to_internal.zip(coerce(other).to_internal).all? { near?(_1, _2) }
  end

  ##
  # Apply the provided block to each color component in turn, returning a new color
  # instance.
  def map(&block) = self.class.from_internal(*to_internal.map(&block))

  ##
  # Apply the provided block to the color component pairs in turn, returning a new color
  # instance.
  def map_with(other, &block) = self.class.from_internal(*zip(other).map(&block))

  ##
  # Zip the color component pairs together.
  def zip(other) = to_internal.zip(coerce(other).to_internal)

  ##
  # Multiplies each component value by the scaling factor or factors, returning a new
  # color object with the scaled values.
  #
  # If a single scaling factor is provided, it is applied to all components:
  #
  # ```ruby
  # rgb = Color::RGB::Wheat # => RGB [#f5deb3]
  # rgb.scale(0.75)         # => RGB [#b8a786]
  # ```
  #
  # If more than one scaling factor is provided, there must be exactly one factor for each
  # color component of the color object or an `ArgumentError` will be raised.
  #
  # ```ruby
  # rgb = Color::RGB::Wheat # => RGB [#f5deb3]
  # # 0xf5 * 0 == 0x00, 0xde * 0.5 == 0x6f, 0xb3 * 2 == 0x166 (clamped to 0xff)
  # rgb.scale(0, 0.5, 2)    # => RGB [#006fff]
  #
  # rgb.scale(1, 2) # => Invalid scaling factors [1, 2] for Color::RGB (ArgumentError)
  # ```
  def scale(*factors)
    if factors.size == 1
      factor = factors.first
      map { _1 * factor }
    elsif factors.size != components
      raise ArgumentError, "Invalid scaling factors #{factors.inspect} for #{self.class}"
    else
      new_components = to_internal.zip(factors).map { _1 * _2 }
      self.class.from_internal(*new_components)
    end
  end

  ##
  def css_value(value, format = nil) # :nodoc:
    if value.nil?
      "none"
    elsif near_zero?(value)
      "0"
    else
      suffix =
        case format
        in :percent
          "%"
        in :degrees
          "deg"
        else
          ""
        end

      "%3.2f%s" % [value, suffix]
    end
  end

  private

  ##
  def from_internal(...) = self.class.from_internal(...)

  ##
  # Returns `true` if the value is less than EPSILON.
  def near_zero?(value) = (value.abs <= Color::EPSILON) # :nodoc:

  ##
  # Returns `true` if the value is within EPSILON of zero or less than zero.
  def near_zero_or_less?(value) = (value < 0.0 or near_zero?(value)) # :nodoc:

  ##
  # Returns +true+ if the value is within EPSILON of one.
  def near_one?(value) = near_zero?(value - 1.0) # :nodoc:

  ##
  # Returns +true+ if the value is within EPSILON of one or more than one.
  def near_one_or_more?(value) = (value > 1.0 or near_one?(value)) # :nodoc:

  ##
  # Returns +true+ if the two values provided are near each other.
  def near?(x, y) = (x - y).abs <= Color::TOLERANCE # :nodoc:

  ##
  def to_degrees(radians) # :nodoc:
    if radians < 0
      (Math::PI + radians % -Math::PI) * (180 / Math::PI) + 180
    else
      (radians % Math::PI) * (180 / Math::PI)
    end
  end

  ##
  def to_radians(degrees) # :nodoc:
    degrees = ((degrees % 360) + 360) % 360
    if degrees >= 180
      Math::PI * (degrees - 360) / 180.0
    else
      Math::PI * degrees / 180.0
    end
  end

  ##
  # Normalizes the value to the range (0.0) .. (1.0).
  module_function def normalize(value, range = 0.0..1.0) # :nodoc:
    value = value.clamp(range)
    if near?(value, range.begin)
      range.begin
    elsif near?(value, range.end)
      range.end
    else
      value
    end
  end

  ##
  # Translates a value from range `from` to range `to`. Both ranges must be closed.
  # As 0.0 .. 1.0 is a common internal range, it is the default for `from`.
  #
  # This is based on the formula:
  #
  #     [a, b] ← from ← [from.begin, from.end]
  #     [c, d] ← to ← [to.begin, to.end]
  #
  #     y = (((x - a) * (d - c)) / (b - a)) + c
  #
  # The value is clamped to the values of `to`.
  module_function def translate_range(x, to:, from: 0.0..1.0) # :nodoc:
    a, b = [from.begin, from.end]
    c, d = [to.begin, to.end]
    y = (((x - a) * (d - c)) / (b - a)) + c
    y.clamp(to)
  end

  ##
  # Normalizes the value to the specified range.
  def normalize_to_range(value, range) # :nodoc:
    range = (range.end..range.begin) if range.end < range.begin

    if value <= range.begin
      range.begin
    elsif value >= range.end
      range.end
    else
      value
    end
  end

  ##
  # Normalize the value to the range (0) .. (255).
  def normalize_byte(value) = normalize_to_range(value, 0..255).to_i # :nodoc:

  ##
  # Normalize the value to the range (0) .. (65535).
  def normalize_word(value) = normalize_to_range(value, 0..65535).to_i # :nodoc:
end

require "color/cmyk"
require "color/grayscale"
require "color/hsl"
require "color/cielab"
require "color/rgb"
require "color/xyz"
require "color/yiq"

require "color/version"