Module: RichEngine::StringColors
- Defined in:
- lib/rich_engine/string_colors.rb
Overview
A refinement that adds color and style methods to String, plus helpers for resolving color specs.
Colors are emitted as 256-color (8-bit) escape sequences using only the theme-independent regions of the palette: the 6x6x6 color cube (16-231) and the grayscale ramp (232-255). Unlike the classic 16 ANSI colors, these render the same RGB values in every terminal.
Anywhere a color is accepted, you can pass:
- a named color (
Symbol)::red,:bright_cyan, ... (see PALETTE) - a hex string:
"#ff8800"(also"ff8800"and shorthand"#f80") - an RGB array:
[255, 136, 0] - a raw 256-color index (
Integer):208
Hex and RGB values snap to the nearest color in the fixed 256-color palette.
Constant Summary collapse
- PALETTE =
Named colors mapped to fixed color-cube/grayscale indices.
{ black: 16, red: 160, green: 40, yellow: 184, orange: 208, blue: 20, magenta: 164, cyan: 44, white: 231, dark_gray: 238, dark_grey: 238, gray: 244, grey: 244, light_gray: 188, light_grey: 188, bright_red: 196, bright_green: 46, bright_yellow: 226, bright_blue: 21, bright_magenta: 201, bright_cyan: 51 }.freeze
- CUBE_LEVELS =
Channel intensities used by the 6x6x6 color cube.
[0, 95, 135, 175, 215, 255].freeze
Class Method Summary collapse
-
.contrast_color(color) ⇒ Symbol
Returns
:blackor:white, whichever has the higher WCAG contrast ratio against the given color, like CSS'scontrast-color()function. -
.gray_index(level) ⇒ Object
private
The grayscale ramp (232-255) covers intensities 8, 18, ...
-
.hex_to_rgb(hex) ⇒ Object
private
Parses
"#rrggbb","rrggbb", or"#rgb"into[r, g, b]values. -
.index_for(color) ⇒ Integer
Resolves a color spec into a 256-color index.
-
.index_to_rgb(index) ⇒ Object
private
Converts a 256-color index (16-255) back into
[r, g, b]values. -
.nearest_cube_level(channel) ⇒ Object
private
Index of the cube level (0-5) closest to the given channel value.
-
.relative_luminance(rgb) ⇒ Object
private
WCAG 2 relative luminance of an sRGB color, from 0.0 (black) to 1.0 (white).
-
.rgb_for(color) ⇒ Object
private
Resolves a color spec into
[r, g, b]channel values. -
.rgb_to_index(red, green, blue) ⇒ Object
private
Snaps
[r, g, b]values to the nearest cube/grayscale index.
Class Method Details
.contrast_color(color) ⇒ Symbol
Returns :black or :white, whichever has the higher WCAG contrast
ratio against the given color, like CSS's contrast-color() function.
Ties go to :white.
99 100 101 102 103 104 105 |
# File 'lib/rich_engine/string_colors.rb', line 99 def self.contrast_color(color) luminance = relative_luminance(rgb_for(color)) white_contrast = 1.05 / (luminance + 0.05) black_contrast = (luminance + 0.05) / 0.05 (white_contrast >= black_contrast) ? :white : :black end |
.gray_index(level) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
The grayscale ramp (232-255) covers intensities 8, 18, ... 238. Levels near the extremes snap to the cube's black (16) and white (231).
181 182 183 184 185 186 |
# File 'lib/rich_engine/string_colors.rb', line 181 def self.gray_index(level) return 16 if level < 4 return 231 if level > 243 232 + ((level - 8) / 10.0).round.clamp(0, 23) end |
.hex_to_rgb(hex) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Parses "#rrggbb", "rrggbb", or "#rgb" into [r, g, b] values.
152 153 154 155 156 |
# File 'lib/rich_engine/string_colors.rb', line 152 def self.hex_to_rgb(hex) digits = hex.delete_prefix("#") digits = digits.each_char.map { |c| c * 2 }.join if digits.length == 3 digits.scan(/../).map { |pair| pair.to_i(16) } end |
.index_for(color) ⇒ Integer
Resolves a color spec into a 256-color index.
73 74 75 76 77 78 79 80 81 82 |
# File 'lib/rich_engine/string_colors.rb', line 73 def self.index_for(color) case color when Symbol then PALETTE.fetch(color) when Integer then color when Array then rgb_to_index(*color) when String then rgb_to_index(*hex_to_rgb(color)) else raise ArgumentError, "invalid color: #{color.inspect}" end end |
.index_to_rgb(index) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Converts a 256-color index (16-255) back into [r, g, b] values.
124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/rich_engine/string_colors.rb', line 124 def self.index_to_rgb(index) if index.between?(16, 231) cube = index - 16 [cube / 36, (cube % 36) / 6, cube % 6].map { |level| CUBE_LEVELS[level] } elsif index.between?(232, 255) level = 8 + (10 * (index - 232)) [level, level, level] else raise ArgumentError, "color index #{index} is theme-dependent; use 16-255" end end |
.nearest_cube_level(channel) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Index of the cube level (0-5) closest to the given channel value.
173 174 175 |
# File 'lib/rich_engine/string_colors.rb', line 173 def self.nearest_cube_level(channel) CUBE_LEVELS.each_index.min_by { |i| (CUBE_LEVELS[i] - channel).abs } end |
.relative_luminance(rgb) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
WCAG 2 relative luminance of an sRGB color, from 0.0 (black) to 1.0 (white). See https://www.w3.org/TR/WCAG20/#relativeluminancedef
140 141 142 143 144 145 146 147 |
# File 'lib/rich_engine/string_colors.rb', line 140 def self.relative_luminance(rgb) r, g, b = rgb.map do |channel| value = channel / 255.0 (value <= 0.04045) ? value / 12.92 : ((value + 0.055) / 1.055)**2.4 end (0.2126 * r) + (0.7152 * g) + (0.0722 * b) end |
.rgb_for(color) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Resolves a color spec into [r, g, b] channel values.
110 111 112 113 114 115 116 117 118 119 |
# File 'lib/rich_engine/string_colors.rb', line 110 def self.rgb_for(color) case color when Symbol then index_to_rgb(PALETTE.fetch(color)) when Integer then index_to_rgb(color) when Array then color when String then hex_to_rgb(color) else raise ArgumentError, "invalid color: #{color.inspect}" end end |
.rgb_to_index(red, green, blue) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Snaps [r, g, b] values to the nearest cube/grayscale index.
161 162 163 164 165 166 167 168 |
# File 'lib/rich_engine/string_colors.rb', line 161 def self.rgb_to_index(red, green, blue) if red == green && green == blue gray_index(red) else r, g, b = [red, green, blue].map { |channel| nearest_cube_level(channel) } 16 + (36 * r) + (6 * g) + b end end |