Class: Pixelart::Color

Inherits:
Object
  • Object
show all
Defined in:
lib/pixelart/colors/color.rb,
lib/pixelart/colors/format.rb

Overview

todo/check - change class to module Color - why? why not?

Constant Summary collapse

TRANSPARENT =

rgba( 0, 0, 0, 0)

0
BLACK =

rgba( 0, 0, 0, 255)

0xff
WHITE =

rgba(255, 255, 255, 255)

0xffffffff
HEX3_COLOR_REGEXP =

The regexp to parse 3-digit hex color values.

/\A(?:#|0x)?([0-9a-f]{3})\z/i
HEX6_COLOR_REGEXP =

The regexp to parse 6- and 8-digit hex color values.

/\A(?:#|0x)?([0-9a-f]{6})([0-9a-f]{2})?\z/i
NAMES =
build_names

Class Method Summary collapse

Class Method Details

.a(value) ⇒ Integer

Returns the alpha channel value for the color value.

Parameters:

  • value (Integer)

    The color value.

Returns:

  • (Integer)

    A value between 0 and MAX.



77
78
79
# File 'lib/pixelart/colors/color.rb', line 77

def self.a(value)
  value & 0x000000ff
end

.b(value) ⇒ Integer

Returns the blue-component from the color value.

Parameters:

  • value (Integer)

    The color value.

Returns:

  • (Integer)

    A value between 0 and MAX.



69
70
71
# File 'lib/pixelart/colors/color.rb', line 69

def self.b(value)
  (value & 0x0000ff00) >> 8
end

.build_namesObject

known built-in color names



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/pixelart/colors/format.rb', line 7

def self.build_names
  names = {
    '#00000000' => 'TRANSPARENT',
    '#000000ff' => 'BLACK',
    '#ffffffff' => 'WHITE',
  }

  ## auto-add grayscale 1 to 254
  (1..254).each do |n|
    hex = "#" + ('%02x' % n)*3
    hex << "ff"  ## add alpha channel (255)
    names[ hex ] = "8-BIT GRAYSCALE ##{n}"
  end

  names
end

.cylindrical_to_cubic(hue, saturation, y_component, chroma) ⇒ Array<Fixnum>

Convert one HSL or HSV triple and associated chroma to a scaled rgb triple

This method encapsulates the shared mathematical operations needed to convert coordinates from a cylindrical colorspace such as HSL or HSV into coordinates of the RGB colorspace.

Even though chroma values are derived from the other three coordinates, the formula for calculating chroma differs for each colorspace. Since it is calculated differently for each colorspace, it must be passed in as a parameter.

Parameters:

  • hue (Fixnum)

    The hue-component (0-360)

  • saturation (Fixnum)

    The saturation-component (0-1)

  • y_component (Fixnum)

    The y_component can represent either lightness or brightness/value (0-1) depending on which scheme (HSV/HSL) is being used.

  • chroma (Fixnum)

    The associated chroma value.

Returns:

  • (Array<Fixnum>)

    A scaled r,g,b triple. Scheme-dependent adjustments are still needed to reach the true r,g,b values.

See Also:



218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/pixelart/colors/color.rb', line 218

def self.cylindrical_to_cubic(hue, saturation, y_component, chroma)
  hue_prime = hue.fdiv(60)
  x = chroma * (1 - (hue_prime % 2 - 1).abs)

  case hue_prime
  when (0...1) then [chroma, x, 0]
  when (1...2) then [x, chroma, 0]
  when (2...3) then [0, chroma, x]
  when (3...4) then [0, x, chroma]
  when (4...5) then [x, 0, chroma]
  when (5..6)  then [chroma, 0, x]
  end
end

.format(color) ⇒ Object Also known as: fmt



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
# File 'lib/pixelart/colors/format.rb', line 28

def self.format( color )
  rgb = [r(color),
         g(color),
         b(color)]

  # rgb in hex (string format)
  #   note: do NOT include alpha channel for now - why? why not?
  hex = "#" + rgb.map{|num| '%02x' % num }.join

  hsl = to_hsl( color )
  hsv = to_hsv( color )

  buf = ''
  buf << hex
  buf << " / "
  buf << "rgb("
  buf << "%3d " % rgb[0]
  buf << "%3d " % rgb[1]
  buf << "%3d)"  % rgb[2]
  buf << " - "
  buf << "hsl("
  buf << "%3d° " % (hsl[0] % 360)
  buf << "%3d%% " % (hsl[1]*100+0.5).to_i
  buf << "%3d%%)" % (hsl[2]*100+0.5).to_i
  buf << " - "
  buf << "hsv("
  buf << "%3d° " % (hsv[0] % 360)
  buf << "%3d%% " % (hsv[1]*100+0.5).to_i
  buf << "%3d%%)" % (hsv[2]*100+0.5).to_i

  alpha = a(color)
  if alpha != 255
    buf << " - α(%3d%%)" % (alpha*100/255+0.5).to_i
  else
    buf << "          "  ## add empty for 255 (full opacity)
  end

  ## note: add alpha channel to hex
  alpha_hex = '%02x' % alpha
  name = NAMES[ hex+alpha_hex ]
  buf << " - #{name}"  if name

  buf
end

.from_hex(hex_str) ⇒ Object



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/pixelart/colors/color.rb', line 117

def self.from_hex( hex_str )
  ## Creates a color by converting it from a string in hex notation.
  ##
  ## It supports colors with (#rrggbbaa) or without (#rrggbb)
  ##  alpha channel as well as the 3-digit short format (#rgb)
  ## for those without. Color strings may include
  ## the prefix "0x" or "#"".
  base_color = case hex_str
               when HEX3_COLOR_REGEXP
                 $1.gsub( /([0-9a-f])/i, '\1\1' ).hex << 8
               when HEX6_COLOR_REGEXP
                 $1.hex << 8
               else
                 raise ArgumentError, "Not a valid hex color notation: #{hex_str.inspect}!"
               end
  opacity = $2 ? $2.hex : 0xff
  base_color | opacity
end

.from_hsl(hue, saturation, lightness, alpha = 255) ⇒ Integer

Creates a new color from an HSL triple.

This implementation follows the modern convention of 0 degrees hue indicating red.

Parameters:

  • hue (Fixnum)

    The hue component (0-360)

  • saturation (Fixnum)

    The saturation component (0-1)

  • lightness (Fixnum)

    The lightness component (0-1)

  • alpha (Fixnum) (defaults to: 255)

    Defaults to opaque (255).

Returns:

  • (Integer)

    The newly constructed color value.

Raises:

  • (ArgumentError)

    if the hsl triple is invalid.

See Also:



161
162
163
164
165
166
167
168
169
170
171
# File 'lib/pixelart/colors/color.rb', line 161

def self.from_hsl( hue, saturation, lightness, alpha=255)
    raise ArgumentError, "Hue #{hue} was not between 0 and 360" unless (0..360).cover?(hue)
    raise ArgumentError, "Saturation #{saturation} was not between 0 and 1" unless (0..1).cover?(saturation)
    raise ArgumentError, "Lightness #{lightness} was not between 0 and 1" unless (0..1).cover?(lightness)

    chroma = (1 - (2 * lightness - 1).abs) * saturation
    rgb    = cylindrical_to_cubic(hue, saturation, lightness, chroma)
    rgb.map! { |component| ((component + lightness - 0.5 * chroma) * 255).to_i }
    rgb << alpha
    rgba(*rgb)
end

.from_hsv(hue, saturation, value, alpha = 255) ⇒ Integer

Creates a new color from an HSV triple.

Create a new color using an HSV (sometimes also called HSB) triple. The words ‘value` and `brightness` are used interchangeably and synonymously in descriptions of this colorspace. This implementation follows the modern convention of 0 degrees hue indicating red.

Parameters:

  • hue (Fixnum)

    The hue component (0-360)

  • saturation (Fixnum)

    The saturation component (0-1)

  • value (Fixnum)

    The value (brightness) component (0-1)

  • alpha (Fixnum) (defaults to: 255)

    Defaults to opaque (255).

Returns:

  • (Integer)

    The newly constructed color value.

Raises:

  • (ArgumentError)

    if the hsv triple is invalid.

See Also:



187
188
189
190
191
192
193
194
195
196
197
# File 'lib/pixelart/colors/color.rb', line 187

def self.from_hsv(hue, saturation, value, alpha=255)
  raise ArgumentError, "Hue must be between 0 and 360" unless (0..360).cover?(hue)
  raise ArgumentError, "Saturation must be between 0 and 1" unless (0..1).cover?(saturation)
  raise ArgumentError, "Value/brightness must be between 0 and 1" unless (0..1).cover?(value)

  chroma = value * saturation
  rgb    = cylindrical_to_cubic(hue, saturation, value, chroma)
  rgb.map! { |component| ((component + value - chroma) * 255).to_i }
  rgb << alpha
  rgba(*rgb)
end

.g(value) ⇒ Integer

Returns the green-component from the color value.

Parameters:

  • value (Integer)

    The color value.

Returns:

  • (Integer)

    A value between 0 and MAX.



61
62
63
# File 'lib/pixelart/colors/color.rb', line 61

def self.g(value)
  (value & 0x00ff0000) >> 16
end

.grayscale?(value) ⇒ true, false

Returns true if this color is fully transparent.

Parameters:

  • value (Integer)

    The color to test.

Returns:

  • (true, false)

    True if the r, g and b component are equal.



96
97
98
# File 'lib/pixelart/colors/color.rb', line 96

def self.grayscale?(value)
  r(value) == b(value) && b(value) == g(value)
end

.hue_and_chroma(color) ⇒ Fixnum

This method encapsulates the logic needed to extract hue and chroma from a color. This logic is shared by the cylindrical HSV/HSB and HSL color space models.

Parameters:

  • color. (Integer)

Returns:

  • (Fixnum)

    hue The hue of the color (0-360)

  • (Fixnum)

    chroma The chroma of the color (0-1)

  • (Fixnum)

    max The magnitude of the largest scaled rgb component (0-1)

  • (Fixnum)

    min The magnitude of the smallest scaled rgb component (0-1)



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/pixelart/colors/color.rb', line 301

def self.hue_and_chroma(color)
  scaled_rgb = [r(color), g(color), b(color)]
  scaled_rgb.map! { |component| component.fdiv(255) }
  min, max = scaled_rgb.minmax
  chroma   = max - min

  r, g, b   = scaled_rgb
  hue_prime = chroma.zero? ? 0 : case max
                                 when r then (g - b).fdiv(chroma)
                                 when g then (b - r).fdiv(chroma) + 2
                                 when b then (r - g).fdiv(chroma) + 4
                                 else 0
                                 end
  hue = 60 * hue_prime

  [hue.round, chroma, max, min]
end

.opaque?(value) ⇒ true, false

Returns true if this color is fully opaque.

Parameters:

  • value (Integer)

    The color to test.

Returns:

  • (true, false)

    True if the alpha channel equals MAX.



88
89
90
# File 'lib/pixelart/colors/color.rb', line 88

def self.opaque?(value)
  a(value) == 0x000000ff
end

.parse(color) ⇒ Object



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# File 'lib/pixelart/colors/color.rb', line 11

def self.parse( color )
  if color.is_a?( Integer )  ## e.g. assumes rgba or such
    color ## pass through as is 1:1
  elsif color.is_a?( Array )  ## assume array of hsl(a) e. g. [180, 0.86, 0.88]
    from_hsl( *color )
  elsif color.is_a?( String )
    if color.downcase == 'transparent'   ## special case for builtin colors
      TRANSPARENT
    else
      ## note: return an Integer !!! (not a Color class or such!!! )
      from_hex( color )
    end
  else
    raise ArgumentError, "unknown color format; cannot parse - expected rgb hex string e.g. d3d3d3"
  end
end

.r(value) ⇒ Integer

Returns the red-component from the color value.

Parameters:

  • value (Integer)

    The color value.

Returns:

  • (Integer)

    A value between 0 and MAX.



53
54
55
# File 'lib/pixelart/colors/color.rb', line 53

def self.r(value)
  (value & 0xff000000) >> 24
end

.rgb(r, g, b) ⇒ Integer

Creates a new color using an r, g, b triple.

Parameters:

  • r (Integer)

    The r-component (0-255)

  • g (Integer)

    The g-component (0-255)

  • b (Integer)

    The b-component (0-255)

Returns:

  • (Integer)

    The newly constructed color value.



44
45
46
# File 'lib/pixelart/colors/color.rb', line 44

def self.rgb(r, g, b)
  r << 24 | g << 16 | b << 8 | 0xff
end

.rgba(r, g, b, a) ⇒ Integer

Creates a new color using an r, g, b triple and an alpha value.

Parameters:

  • r (Integer)

    The r-component (0-255)

  • g (Integer)

    The g-component (0-255)

  • b (Integer)

    The b-component (0-255)

  • a (Integer)

    The opacity (0-255)

Returns:

  • (Integer)

    The newly constructed color value.



35
36
37
# File 'lib/pixelart/colors/color.rb', line 35

def self.rgba(r, g, b, a)
  r << 24 | g << 16 | b << 8 | a
end

.to_hex(color, include_alpha: true) ⇒ String

Returns a string representing this color using hex notation (i.e. #rrggbbaa).

Parameters:

  • color (Integer)

    The color to convert.

  • include_alpha (Boolean) (defaults to: true)

Returns:

  • (String)

    The color in hex notation, starting with a pound sign.



142
143
144
# File 'lib/pixelart/colors/color.rb', line 142

def self.to_hex( color, include_alpha: true )
    include_alpha ? ("#%08x" % color) : ("#%06x" % [color >> 8])
end

.to_hsl(color, include_alpha: true) ⇒ Array<Fixnum>[0], ...

Note: Because this code internally handles colors as Integers for performance reasons, some rounding occurs when importing or exporting HSL colors whose coordinates are float-based. Because of this rounding, #to_hsl and #from_hsl may not be perfect inverses.

This implementation follows the modern convention of 0 degrees hue indicating red.

Parameters:

  • color (Integer)

    The color to convert.

  • include_alpha (Boolean) (defaults to: true)

    Flag indicates whether a fourth element representing alpha channel should be included in the returned array.

Returns:

  • (Array<Fixnum>[0])

    The hue of the color (0-360)

  • (Array<Fixnum>[1])

    The saturation of the color (0-1)

  • (Array<Fixnum>[2])

    The lightness of the color (0-1)

  • (Array<Fixnum>[3])

    Optional fourth element for alpha, included if include_alpha=true (0-255)

See Also:



252
253
254
255
256
257
258
259
# File 'lib/pixelart/colors/color.rb', line 252

def self.to_hsl(color, include_alpha: true )
  hue, chroma, max, min = hue_and_chroma(color)
  lightness  = 0.5 * (max + min)
  saturation = chroma.zero? ? 0.0 : chroma.fdiv(1 - (2 * lightness - 1).abs)

  include_alpha ? [hue, saturation, lightness, a(color)] :
                  [hue, saturation, lightness]
end

.to_hsv(color, include_alpha: true) ⇒ Array[0], ...

Returns an array with the separate HSV components of a color.

Note: Because this code internally handles colors as Integers for performance reasons, some rounding occurs when importing or exporting HSV colors whose coordinates are float-based. Because of this rounding, #to_hsv and #from_hsv may not be perfect inverses.

This implementation follows the modern convention of 0 degrees hue indicating red.

Parameters:

  • color (Integer)
  • include_alpha (Boolean) (defaults to: true)

    Flag indicates whether a fourth element representing alpha channel should be included in the returned array.

Returns:

  • (Array[0])

    The hue of the color (0-360)

  • (Array[1])

    The saturation of the color (0-1)

  • (Array[2])

    The value of the color (0-1)

  • (Array[3])

    Optional fourth element for alpha, included if include_alpha=true (0-255)

See Also:



280
281
282
283
284
285
286
287
# File 'lib/pixelart/colors/color.rb', line 280

def self.to_hsv(color, include_alpha: true )
  hue, chroma, max, _ = hue_and_chroma(color)
  value      = max
  saturation = chroma.zero? ? 0.0 : chroma.fdiv(value)

  include_alpha ? [hue, saturation, value, a(color)] :
                  [hue, saturation, value]
end

.transparent?(value) ⇒ true, false

Returns true if this color is fully transparent.

Parameters:

  • value (Integer)

    The color to test.

Returns:

  • (true, false)

    True if the alpha channel equals 0.



104
105
106
# File 'lib/pixelart/colors/color.rb', line 104

def self.transparent?(value)
  a(value) == 0x00000000
end