Class: Ruby2D::Color

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby2d/color.rb

Overview

A color from a keyword, hex value, or RGBA array

Defined Under Namespace

Classes: Set

Constant Summary collapse

PARSE_CACHE_MAX =

Bounded cache of parsed color strings, keyed by the user input. 'random' is never cached (re-randomized on each call).

256
RENDER_CACHE_MAX =

Bounded cache of shared Color instances for immediate-mode .render calls, keyed by the color string. See Color.for_render.

256
NAMED_COLORS =

Based on clrs.cc

{
  'navy' => '#001F3F',
  'blue' => '#0074D9',
  'aqua' => '#7FDBFF',
  'teal' => '#39CCCC',
  'olive' => '#3D9970',
  'green' => '#2ECC40',
  'lime' => '#01FF70',
  'yellow' => '#FFDC00',
  'orange' => '#FF851B',
  'red' => '#FF4136',
  'brown' => '#663300',
  'fuchsia' => '#F012BE',
  'purple' => '#B10DC9',
  'maroon' => '#85144B',
  'white' => '#FFFFFF',
  'silver' => '#DDDDDD',
  'gray' => '#AAAAAA',
  'black' => '#111111',
  'random' => ''
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(color) ⇒ Color

Create a color from a keyword, hex string, array, or Color

Raises:



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/ruby2d/color.rb', line 102

def initialize(color)
  raise Error, "#{color.inspect} is not a valid color" unless self.class.valid? color

  case color
  when String
    init_from_string color
  when Array
    @r = channel(color[0])
    @g = channel(color[1])
    @b = channel(color[2])
    @a = color.length == 4 ? channel(color[3]) : 1.0
  when Color
    @r = color.r
    @g = color.g
    @b = color.b
    @a = color.a
  end
end

Instance Attribute Details

#aObject

Returns the value of attribute a.



66
67
68
# File 'lib/ruby2d/color.rb', line 66

def a
  @a
end

#bObject

Returns the value of attribute b.



66
67
68
# File 'lib/ruby2d/color.rb', line 66

def b
  @b
end

#gObject

Returns the value of attribute g.



66
67
68
# File 'lib/ruby2d/color.rb', line 66

def g
  @g
end

#rObject

Returns the value of attribute r.



66
67
68
# File 'lib/ruby2d/color.rb', line 66

def r
  @r
end

Class Method Details

.for_render(colors) ⇒ Object

Resolve a color for an immediate-mode class-level .render call. Behaves like .set, except string colors and flat [r, g, b(, a)] numeric arrays return a shared cached Color instance, skipping the validation re-scan and object allocation .new pays on every call — those two forms are the common case in per-frame draws, and on the web (mruby/wasm) that per-call cost dominates the frame budget. The cached instance is read and forwarded to the native draw call immediately; it must never be stored on an object or handed to user code (a later mutation would corrupt every subsequent lookup) — use .set anywhere the color is kept. 'random' is never cached, so each call still rolls a fresh color.



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
# File 'lib/ruby2d/color.rb', line 149

def for_render(colors)
  if colors.is_a?(String)
    cached = @render_cache[colors]
    return cached if cached
    return Color.new(colors) if colors == 'random'

    @render_cache.shift if @render_cache.size >= RENDER_CACHE_MAX
    @render_cache[colors] = Color.new(colors)
  elsif colors.is_a?(Array) && colors[0].is_a?(Numeric) && rgba_array?(colors)
    # ^ The two inline checks pre-screen the non-match cases (an array of
    # colors starts with a String/Array/Color, never a Numeric) so this
    # per-draw-call path only pays the `rgba_array?` method call when the
    # input is almost certainly a cacheable [r, g, b(, a)] array.
    # Array keys are looked up by value, so mutating a previously seen
    # array can't corrupt the mapping — it just misses and inserts a new
    # entry. The stored key is a frozen copy for the same reason.
    cached = @render_cache[colors]
    return cached if cached

    c = Color.new(colors)
    @render_cache.shift if @render_cache.size >= RENDER_CACHE_MAX
    @render_cache[colors.dup.freeze] = c
  else
    set(colors)
  end
end

.hex?(color_string) ⇒ Boolean

Check if the string is a valid hex color value Byte comparisons, not slicing: valid? calls this on every Color.new and on every per-vertex color of every immediate-mode draw, and the readable form ([0], [1..], .chars, plus a fresh literal for the allowed set) allocated about ten short-lived strings each time.

Returns:

  • (Boolean)


199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/ruby2d/color.rb', line 199

def hex?(color_string)
  return false unless color_string.instance_of?(String) &&
                      color_string.getbyte(0) == 35 # '#'

  len = color_string.length
  return false unless len == 4 || len == 7 || len == 9

  i = 1
  while i < len
    b = color_string.getbyte(i)
    return false unless (b >= 48 && b <= 57) ||   # 0-9
                        (b >= 65 && b <= 70) ||   # A-F
                        (b >= 97 && b <= 102)     # a-f

    i += 1
  end
  true
end

.parse_string(color) ⇒ Object

Parse a color string into a frozen [r, g, b, a] tuple, caching the result. Named colors and hex strings are cached; 'random' is not. Callers must not mutate the returned array.



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/ruby2d/color.rb', line 233

def parse_string(color)
  # Check the cache first so the hot path (a previously-seen named or hex
  # color) returns before the `'random'` literal comparison, which would
  # otherwise allocate a fresh `'random'` string on every call. `'random'`
  # is never cached, so it still falls through to a fresh value below.
  cached = @parse_cache[color]
  return cached if cached

  return [rand, rand, rand, 1.0] if color == 'random'

  source = hex?(color) ? color : NAMED_COLORS[color]
  rgba = hex_to_f(source).freeze
  @parse_cache.shift if @parse_cache.size >= PARSE_CACHE_MAX
  @parse_cache[color] = rgba
end

.rgba_array?(colors) ⇒ Boolean

A flat [r, g, b] or [r, g, b, a] numeric array — the array form a single color takes (an array of colors is a Color::Set, and its elements are never bare Numerics). while, not blocks — this guards the per-draw-call render path and block calls dominate on wasm mruby.

Returns:

  • (Boolean)


180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/ruby2d/color.rb', line 180

def rgba_array?(colors)
  return false unless colors.is_a?(Array)

  n = colors.length
  return false unless n == 3 || n == 4

  i = 0
  while i < n
    return false unless colors[i].is_a?(Numeric)
    i += 1
  end
  true
end

.set(colors) ⇒ Object

Create a Color or Color::Set from the given value



123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/ruby2d/color.rb', line 123

def set(colors)
  # Already a Color::Set (e.g. re-applying a gradient fill): pass through.
  return colors if colors.is_a?(Color::Set)

  # A non-empty array of valid colors becomes a `Color::Set`. An empty
  # array is not a valid gradient, so it falls through to `Color.new`,
  # which raises a clear "not a valid color" at the mistake site.
  if colors.is_a?(Array) && !colors.empty? && colors.all? { |el| Color.valid? el }
    Color::Set.new(colors)
  # Otherwise, return single color
  else
    Color.new(colors)
  end
end

.valid?(color) ⇒ Boolean

Check if the value is a valid color

Returns:

  • (Boolean)


219
220
221
222
223
224
225
226
227
228
# File 'lib/ruby2d/color.rb', line 219

def valid?(color)
  color.is_a?(Color) ||             # color object
    NAMED_COLORS.key?(color) ||     # keyword
    hex?(color) ||                  # hexadecimal value
    (                               # [r, g, b] or [r, g, b, a] numbers
      color.instance_of?(Array) &&
      (color.length == 3 || color.length == 4) &&
      color.all? { |el| el.is_a?(Numeric) }
    )
end

Instance Method Details

#opacityObject

Get the opacity



263
264
265
# File 'lib/ruby2d/color.rb', line 263

def opacity
  @a
end

#opacity=(opacity) ⇒ Object

Set the opacity. Must be a single number; per-vertex opacity (an array) is only supported by shapes that handle it explicitly, such as Polyline. The value is clamped to 0.0..1.0 so an animation that momentarily drives opacity out of range degrades to fully transparent/opaque rather than wrapping the Uint8 alpha cast into a wrong, near-opaque byte.



272
273
274
275
276
277
278
# File 'lib/ruby2d/color.rb', line 272

def opacity=(opacity)
  unless opacity.is_a?(Numeric)
    raise ArgumentError, "opacity must be a number between 0.0 and 1.0, got #{opacity.inspect}"
  end

  @a = opacity.clamp(0.0, 1.0)
end

#to_aObject

Return the color components as an array



281
282
283
# File 'lib/ruby2d/color.rb', line 281

def to_a
  [@r, @g, @b, @a]
end

#vertex(_i) ⇒ Object

The color for the i-th vertex. A single Color represents "every vertex is the same color," so every index returns self. Mirrors Color::Set#vertex so per-vertex rendering code can call color.vertex(i) without branching.



288
289
290
# File 'lib/ruby2d/color.rb', line 288

def vertex(_i)
  self
end