sai 0.3.0 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -1
- data/README.md +11 -3
- data/docs/USAGE.md +57 -9
- data/lib/sai/ansi/color_parser.rb +109 -0
- data/lib/sai/ansi/sequence_processor.rb +15 -126
- data/lib/sai/ansi/style_parser.rb +66 -0
- data/lib/sai/ansi.rb +0 -27
- data/lib/sai/conversion/color_sequence.rb +4 -4
- data/lib/sai/conversion/rgb/color_classifier.rb +209 -0
- data/lib/sai/conversion/rgb/color_indexer.rb +48 -0
- data/lib/sai/conversion/rgb/color_space.rb +192 -0
- data/lib/sai/conversion/rgb/color_transformer.rb +140 -0
- data/lib/sai/conversion/rgb.rb +23 -269
- data/lib/sai/decorator/color_manipulations.rb +157 -0
- data/lib/sai/decorator/delegation.rb +84 -0
- data/lib/sai/decorator/gradients.rb +363 -0
- data/lib/sai/decorator/hex_colors.rb +56 -0
- data/lib/sai/decorator/named_colors.rb +780 -0
- data/lib/sai/decorator/named_styles.rb +276 -0
- data/lib/sai/decorator/rgb_colors.rb +64 -0
- data/lib/sai/decorator.rb +29 -775
- data/lib/sai/named_colors.rb +437 -0
- data/lib/sai.rb +731 -23
- data/sig/sai/ansi/color_parser.rbs +77 -0
- data/sig/sai/ansi/sequence_processor.rbs +0 -75
- data/sig/sai/ansi/style_parser.rbs +59 -0
- data/sig/sai/ansi.rbs +0 -10
- data/sig/sai/conversion/rgb/color_classifier.rbs +165 -0
- data/sig/sai/conversion/rgb/color_indexer.rbs +41 -0
- data/sig/sai/conversion/rgb/color_space.rbs +129 -0
- data/sig/sai/conversion/rgb/color_transformer.rbs +99 -0
- data/sig/sai/conversion/rgb.rbs +15 -198
- data/sig/sai/decorator/color_manipulations.rbs +125 -0
- data/sig/sai/decorator/delegation.rbs +47 -0
- data/sig/sai/decorator/gradients.rbs +267 -0
- data/sig/sai/decorator/hex_colors.rbs +48 -0
- data/sig/sai/decorator/named_colors.rbs +1491 -0
- data/sig/sai/decorator/named_styles.rbs +72 -0
- data/sig/sai/decorator/rgb_colors.rbs +52 -0
- data/sig/sai/decorator.rbs +21 -195
- data/sig/sai/named_colors.rbs +65 -0
- data/sig/sai.rbs +1468 -44
- metadata +32 -4
@@ -0,0 +1,209 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sai
|
4
|
+
module Conversion
|
5
|
+
module RGB
|
6
|
+
# Classify color characteristics
|
7
|
+
#
|
8
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
9
|
+
# @since 0.3.1
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
module ColorClassifier
|
13
|
+
class << self
|
14
|
+
# Get closest ANSI color for RGB values
|
15
|
+
#
|
16
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
17
|
+
# @since 0.1.0
|
18
|
+
#
|
19
|
+
# @api private
|
20
|
+
#
|
21
|
+
# @param red [Float] the red component (0-1)
|
22
|
+
# @param green [Float] the green component (0-1)
|
23
|
+
# @param blue [Float] the blue component (0-1)
|
24
|
+
#
|
25
|
+
# @return [Symbol] the closest ANSI color name
|
26
|
+
# @rbs (Float red, Float green, Float blue) -> Symbol
|
27
|
+
def closest_ansi_color(red, green, blue)
|
28
|
+
return :black if dark?(red, green, blue)
|
29
|
+
return :white if grayscale?(red, green, blue)
|
30
|
+
return primary_color(red, green, blue) if primary?(red, green, blue)
|
31
|
+
return secondary_color(red, green, blue) if secondary?(red, green, blue)
|
32
|
+
|
33
|
+
:white
|
34
|
+
end
|
35
|
+
|
36
|
+
# Determine if a color is dark
|
37
|
+
#
|
38
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
39
|
+
# @since 0.1.0
|
40
|
+
#
|
41
|
+
# @api private
|
42
|
+
#
|
43
|
+
# @param red [Float] the red component (0-1)
|
44
|
+
# @param green [Float] the green component (0-1)
|
45
|
+
# @param blue [Float] the blue component (0-1)
|
46
|
+
#
|
47
|
+
# @return [Boolean] true if color is dark
|
48
|
+
# @rbs (Float red, Float green, Float blue) -> bool
|
49
|
+
def dark?(red, green, blue)
|
50
|
+
[red, green, blue].max < 0.3
|
51
|
+
end
|
52
|
+
|
53
|
+
# Determine if a color is grayscale
|
54
|
+
#
|
55
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
56
|
+
# @since 0.1.0
|
57
|
+
#
|
58
|
+
# @api private
|
59
|
+
#
|
60
|
+
# @param red [Float] the red component (0-1)
|
61
|
+
# @param green [Float] the green component (0-1)
|
62
|
+
# @param blue [Float] the blue component (0-1)
|
63
|
+
#
|
64
|
+
# @return [Boolean] true if color is grayscale
|
65
|
+
# @rbs (Float red, Float green, Float blue) -> bool
|
66
|
+
def grayscale?(red, green, blue)
|
67
|
+
red == green && green == blue
|
68
|
+
end
|
69
|
+
|
70
|
+
# Determine if RGB values represent a primary color
|
71
|
+
#
|
72
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
73
|
+
# @since 0.1.0
|
74
|
+
#
|
75
|
+
# @api private
|
76
|
+
#
|
77
|
+
# @param red [Float] the red component (0-1)
|
78
|
+
# @param green [Float] the green component (0-1)
|
79
|
+
# @param blue [Float] the blue component (0-1)
|
80
|
+
#
|
81
|
+
# @return [Boolean] true if color is primary
|
82
|
+
# @rbs (Float red, Float green, Float blue) -> bool
|
83
|
+
def primary?(red, green, blue)
|
84
|
+
max = [red, green, blue].max
|
85
|
+
mid = [red, green, blue].sort[1]
|
86
|
+
(max - mid) > 0.3
|
87
|
+
end
|
88
|
+
|
89
|
+
# Get the closest primary color
|
90
|
+
#
|
91
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
92
|
+
# @since 0.1.0
|
93
|
+
#
|
94
|
+
# @api private
|
95
|
+
#
|
96
|
+
# @param red [Float] the red component (0-1)
|
97
|
+
# @param green [Float] the green component (0-1)
|
98
|
+
# @param blue [Float] the blue component (0-1)
|
99
|
+
#
|
100
|
+
# @return [Symbol] the primary color name
|
101
|
+
# @rbs (Float red, Float green, Float blue) -> Symbol
|
102
|
+
def primary_color(red, green, blue)
|
103
|
+
max = [red, green, blue].max
|
104
|
+
case max
|
105
|
+
when red then :red
|
106
|
+
when green then :green
|
107
|
+
else :blue
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Determine if RGB values represent a secondary color
|
112
|
+
#
|
113
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
114
|
+
# @since 0.1.0
|
115
|
+
#
|
116
|
+
# @api private
|
117
|
+
#
|
118
|
+
# @param red [Float] the red component (0-1)
|
119
|
+
# @param green [Float] the green component (0-1)
|
120
|
+
# @param blue [Float] the blue component (0-1)
|
121
|
+
#
|
122
|
+
# @return [Boolean] true if color is secondary
|
123
|
+
# @rbs (Float red, Float green, Float blue) -> bool
|
124
|
+
def secondary?(red, green, blue)
|
125
|
+
return true if yellow?(red, green, blue)
|
126
|
+
return true if magenta?(red, green, blue)
|
127
|
+
return true if cyan?(red, green, blue)
|
128
|
+
|
129
|
+
false
|
130
|
+
end
|
131
|
+
|
132
|
+
# Get the closest secondary color
|
133
|
+
#
|
134
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
135
|
+
# @since 0.1.0
|
136
|
+
#
|
137
|
+
# @api private
|
138
|
+
#
|
139
|
+
# @param red [Float] the red component (0-1)
|
140
|
+
# @param green [Float] the green component (0-1)
|
141
|
+
# @param blue [Float] the blue component (0-1)
|
142
|
+
#
|
143
|
+
# @return [Symbol] the secondary color name
|
144
|
+
# @rbs (Float red, Float green, Float blue) -> Symbol
|
145
|
+
def secondary_color(red, green, blue)
|
146
|
+
return :yellow if yellow?(red, green, blue)
|
147
|
+
return :magenta if magenta?(red, green, blue)
|
148
|
+
return :cyan if cyan?(red, green, blue)
|
149
|
+
|
150
|
+
:white
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
# Check if RGB values represent cyan
|
156
|
+
#
|
157
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
158
|
+
# @since 0.1.0
|
159
|
+
#
|
160
|
+
# @api private
|
161
|
+
#
|
162
|
+
# @param red [Float] the red component (0-1)
|
163
|
+
# @param green [Float] the green component (0-1)
|
164
|
+
# @param blue [Float] the blue component (0-1)
|
165
|
+
#
|
166
|
+
# @return [Boolean] true if color is cyan
|
167
|
+
# @rbs (Float red, Float green, Float blue) -> bool
|
168
|
+
def cyan?(red, green, blue)
|
169
|
+
green > red && blue > red
|
170
|
+
end
|
171
|
+
|
172
|
+
# Check if RGB values represent magenta
|
173
|
+
#
|
174
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
175
|
+
# @since 0.1.0
|
176
|
+
#
|
177
|
+
# @api private
|
178
|
+
#
|
179
|
+
# @param red [Float] the red component (0-1)
|
180
|
+
# @param green [Float] the green component (0-1)
|
181
|
+
# @param blue [Float] the blue component (0-1)
|
182
|
+
#
|
183
|
+
# @return [Boolean] true if color is magenta
|
184
|
+
# @rbs (Float red, Float green, Float blue) -> bool
|
185
|
+
def magenta?(red, green, blue)
|
186
|
+
red > green && blue > green
|
187
|
+
end
|
188
|
+
|
189
|
+
# Check if RGB values represent yellow
|
190
|
+
#
|
191
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
192
|
+
# @since 0.1.0
|
193
|
+
#
|
194
|
+
# @api private
|
195
|
+
#
|
196
|
+
# @param red [Float] the red component (0-1)
|
197
|
+
# @param green [Float] the green component (0-1)
|
198
|
+
# @param blue [Float] the blue component (0-1)
|
199
|
+
#
|
200
|
+
# @return [Boolean] true if color is yellow
|
201
|
+
# @rbs (Float red, Float green, Float blue) -> bool
|
202
|
+
def yellow?(red, green, blue)
|
203
|
+
red > blue && green > blue && (red - green).abs < 0.3
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sai
|
4
|
+
module Conversion
|
5
|
+
module RGB
|
6
|
+
# Color indexing utilities
|
7
|
+
#
|
8
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
9
|
+
# @since 0.3.1
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
module ColorIndexer
|
13
|
+
class << self
|
14
|
+
# Convert RGB values to 256-color cube index
|
15
|
+
#
|
16
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
17
|
+
# @since 0.1.0
|
18
|
+
#
|
19
|
+
# @api private
|
20
|
+
#
|
21
|
+
# @param rgb [Array<Integer>] RGB values (0-255)
|
22
|
+
#
|
23
|
+
# @return [Integer] the color cube index
|
24
|
+
# @rbs (Array[Integer] rgb) -> Integer
|
25
|
+
def color_cube(rgb)
|
26
|
+
r, g, b = rgb.map { |c| ((c / 255.0) * 5).round } #: [Integer, Integer, Integer]
|
27
|
+
16 + (r * 36) + (g * 6) + b
|
28
|
+
end
|
29
|
+
|
30
|
+
# Convert RGB values to grayscale index
|
31
|
+
#
|
32
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
33
|
+
# @since 0.1.0
|
34
|
+
#
|
35
|
+
# @api private
|
36
|
+
#
|
37
|
+
# @param rgb [Array<Integer>] RGB values
|
38
|
+
#
|
39
|
+
# @return [Integer] the grayscale index
|
40
|
+
# @rbs (Array[Integer] rgb) -> Integer
|
41
|
+
def grayscale(rgb)
|
42
|
+
232 + ((rgb[0] / 255.0) * 23).round
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sai/named_colors'
|
4
|
+
|
5
|
+
module Sai
|
6
|
+
module Conversion
|
7
|
+
module RGB
|
8
|
+
# Convert colors between different color space formats
|
9
|
+
#
|
10
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
11
|
+
# @since 0.3.1
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
module ColorSpace
|
15
|
+
class << self
|
16
|
+
# Convert HSV values to RGB
|
17
|
+
#
|
18
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
19
|
+
# @since 0.3.1
|
20
|
+
#
|
21
|
+
# @api private
|
22
|
+
#
|
23
|
+
# @param hue [Float] the hue component (0-360)
|
24
|
+
# @param saturation [Float] the saturation component (0-1)
|
25
|
+
# @param value [Float] the value component (0-1)
|
26
|
+
#
|
27
|
+
# @return [Array<Integer>] the RGB values
|
28
|
+
# @rbs (Float hue, Float saturation, Float value) -> Array[Integer]
|
29
|
+
def hsv_to_rgb(hue, saturation, value)
|
30
|
+
hue_sector = (hue / 60.0).floor.to_i
|
31
|
+
hue_remainder = (hue / 60.0) - hue_sector
|
32
|
+
|
33
|
+
components = calculate_hsv_components(value, saturation, hue_remainder)
|
34
|
+
primary, secondary, tertiary = *components
|
35
|
+
|
36
|
+
return [0, 0, 0] unless primary && secondary && tertiary
|
37
|
+
|
38
|
+
rgb = select_rgb_values(hue_sector, value, primary, secondary, tertiary)
|
39
|
+
normalize_rgb(rgb)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Convert a color value to RGB components
|
43
|
+
#
|
44
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
45
|
+
# @since 0.1.0
|
46
|
+
#
|
47
|
+
# @api private
|
48
|
+
#
|
49
|
+
# @param color [String, Array<Integer>] the color to convert
|
50
|
+
#
|
51
|
+
# @raise [ArgumentError] if the color format is invalid
|
52
|
+
# @return [Array<Integer>] the RGB components
|
53
|
+
# @rbs (Array[Integer] | String | Symbol color) -> Array[Integer]
|
54
|
+
def resolve(color)
|
55
|
+
case color
|
56
|
+
when Array then validate_rgb(color)
|
57
|
+
when /^#?([A-Fa-f0-9]{6})$/
|
58
|
+
hex_to_rgb(
|
59
|
+
Regexp.last_match(1) # steep:ignore ArgumentTypeMismatch
|
60
|
+
)
|
61
|
+
when String, Symbol then named_to_rgb(color.to_s.downcase)
|
62
|
+
else
|
63
|
+
raise ArgumentError, "Invalid color format: #{color}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# Calculate the intermediate HSV components
|
70
|
+
#
|
71
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
72
|
+
# @since 0.3.1
|
73
|
+
#
|
74
|
+
# @api private
|
75
|
+
#
|
76
|
+
# @param value [Float] the value component
|
77
|
+
# @param saturation [Float] the saturation component
|
78
|
+
# @param hue_remainder [Float] the remainder of hue / 60
|
79
|
+
#
|
80
|
+
# @return [Array<Float>] the primary, secondary, and tertiary components
|
81
|
+
# @rbs (Float value, Float saturation, Float hue_remainder) -> [Float, Float, Float]
|
82
|
+
def calculate_hsv_components(value, saturation, hue_remainder)
|
83
|
+
primary = value * (1 - saturation)
|
84
|
+
secondary = value * (1 - (saturation * hue_remainder))
|
85
|
+
tertiary = value * (1 - (saturation * (1 - hue_remainder)))
|
86
|
+
|
87
|
+
[primary, secondary, tertiary]
|
88
|
+
end
|
89
|
+
|
90
|
+
# Convert a hex string to RGB values
|
91
|
+
#
|
92
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
93
|
+
# @since 0.1.0
|
94
|
+
#
|
95
|
+
# @api private
|
96
|
+
#
|
97
|
+
# @param hex [String] the hex color code
|
98
|
+
#
|
99
|
+
# @return [Array<Integer>] the RGB components
|
100
|
+
# @rbs (String hex) -> Array[Integer]
|
101
|
+
def hex_to_rgb(hex)
|
102
|
+
hex = hex.delete_prefix('#')
|
103
|
+
[
|
104
|
+
hex[0..1].to_i(16), # steep:ignore UnexpectedPositionalArgument
|
105
|
+
hex[2..3].to_i(16), # steep:ignore UnexpectedPositionalArgument
|
106
|
+
hex[4..5].to_i(16) # steep:ignore UnexpectedPositionalArgument
|
107
|
+
]
|
108
|
+
end
|
109
|
+
|
110
|
+
# Convert a named color to RGB values
|
111
|
+
#
|
112
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
113
|
+
# @since 0.1.0
|
114
|
+
#
|
115
|
+
# @api private
|
116
|
+
#
|
117
|
+
# @param color_name [String] the color name
|
118
|
+
#
|
119
|
+
# @raise [ArgumentError] if the color name is unknown
|
120
|
+
# @return [Array<Integer>] the RGB components
|
121
|
+
# @rbs (String color_name) -> Array[Integer]
|
122
|
+
def named_to_rgb(color_name)
|
123
|
+
color = NamedColors[color_name.to_sym]
|
124
|
+
raise ArgumentError, "Unknown color name: #{color_name}" unless color
|
125
|
+
|
126
|
+
color
|
127
|
+
end
|
128
|
+
|
129
|
+
# Convert RGB values from 0-1 range to 0-255 range
|
130
|
+
#
|
131
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
132
|
+
# @since 0.3.1
|
133
|
+
#
|
134
|
+
# @api private
|
135
|
+
#
|
136
|
+
# @param rgb [Array<Float>] RGB values in 0-1 range
|
137
|
+
#
|
138
|
+
# @return [Array<Integer>] RGB values in 0-255 range
|
139
|
+
# @rbs (Array[Float] rgb) -> Array[Integer]
|
140
|
+
def normalize_rgb(rgb)
|
141
|
+
rgb.map { |c| (c * 255).round.clamp(0, 255) }
|
142
|
+
end
|
143
|
+
|
144
|
+
# Select RGB values based on the hue sector
|
145
|
+
#
|
146
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
147
|
+
# @since 0.3.1
|
148
|
+
#
|
149
|
+
# @api private
|
150
|
+
#
|
151
|
+
# @param sector [Integer] the hue sector (0-5)
|
152
|
+
# @param value [Float] the value component
|
153
|
+
# @param primary [Float] primary component from HSV calculation
|
154
|
+
# @param secondary [Float] secondary component from HSV calculation
|
155
|
+
# @param tertiary [Float] tertiary component from HSV calculation
|
156
|
+
#
|
157
|
+
# @return [Array<Float>] the RGB values before normalization
|
158
|
+
# @rbs (Integer sector, Float value, Float primary, Float secondary, Float tertiary) -> Array[Float]
|
159
|
+
def select_rgb_values(sector, value, primary, secondary, tertiary)
|
160
|
+
case sector % 6
|
161
|
+
when 0 then [value, tertiary, primary]
|
162
|
+
when 1 then [secondary, value, primary]
|
163
|
+
when 2 then [primary, value, tertiary]
|
164
|
+
when 3 then [primary, secondary, value]
|
165
|
+
when 4 then [tertiary, primary, value]
|
166
|
+
else [value, primary, secondary]
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Validate RGB values
|
171
|
+
#
|
172
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
173
|
+
# @since 0.1.0
|
174
|
+
#
|
175
|
+
# @api private
|
176
|
+
#
|
177
|
+
# @param color [Array<Integer>] the RGB components to validate
|
178
|
+
# @return [Array<Integer>] the validated RGB components
|
179
|
+
# @raise [ArgumentError] if the RGB values are invalid
|
180
|
+
# @rbs (Array[Integer] color) -> Array[Integer]
|
181
|
+
def validate_rgb(color)
|
182
|
+
unless color.size == 3 && color.all? { |c| c.is_a?(Integer) && c.between?(0, 255) }
|
183
|
+
raise ArgumentError, "Invalid RGB values: #{color}"
|
184
|
+
end
|
185
|
+
|
186
|
+
color
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sai/conversion/rgb/color_space'
|
4
|
+
|
5
|
+
module Sai
|
6
|
+
module Conversion
|
7
|
+
module RGB
|
8
|
+
# Perform color transformations
|
9
|
+
#
|
10
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
11
|
+
# @since 0.3.1
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
module ColorTransformer
|
15
|
+
class << self
|
16
|
+
# Darken an RGB color by a percentage
|
17
|
+
#
|
18
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
19
|
+
# @since 0.3.1
|
20
|
+
#
|
21
|
+
# @api private
|
22
|
+
#
|
23
|
+
# @param color [Array<Integer>, String, Symbol] the color to darken
|
24
|
+
# @param amount [Float] amount to darken by (0.0-1.0)
|
25
|
+
#
|
26
|
+
# @raise [ArgumentError] if amount is not between 0.0 and 1.0
|
27
|
+
# @return [Array<Integer>] the darkened RGB values
|
28
|
+
# @rbs ((Array[Integer] | String | Symbol) color, Float amount) -> Array[Integer]
|
29
|
+
def darken(color, amount)
|
30
|
+
raise ArgumentError, "Invalid amount: #{amount}" unless amount.between?(0.0, 1.0)
|
31
|
+
|
32
|
+
rgb = ColorSpace.resolve(color)
|
33
|
+
rgb.map { |c| [0, (c * (1 - amount)).round].max }
|
34
|
+
end
|
35
|
+
|
36
|
+
# Generate a gradient between two colors with a specified number of steps
|
37
|
+
#
|
38
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
39
|
+
# @since 0.3.1
|
40
|
+
#
|
41
|
+
# @api private
|
42
|
+
#
|
43
|
+
# @param start_color [Array<Integer>, String, Symbol] the starting color
|
44
|
+
# @param end_color [Array<Integer>, String, Symbol] the ending color
|
45
|
+
# @param steps [Integer] the number of colors to generate (minimum 2)
|
46
|
+
#
|
47
|
+
# @raise [ArgumentError] if steps is less than 2
|
48
|
+
# @return [Array<Array<Integer>>] the gradient colors as RGB values
|
49
|
+
# @rbs (
|
50
|
+
# (Array[Integer] | String | Symbol) start_color,
|
51
|
+
# (Array[Integer] | String | Symbol) end_color,
|
52
|
+
# Integer steps
|
53
|
+
# ) -> Array[Array[Integer]]
|
54
|
+
def gradient(start_color, end_color, steps)
|
55
|
+
raise ArgumentError, "Steps must be at least 2, got: #{steps}" if steps < 2
|
56
|
+
|
57
|
+
(0...steps).map do |i|
|
58
|
+
step = i.to_f / (steps - 1)
|
59
|
+
interpolate_color(start_color, end_color, step)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Interpolate between two colors to create a gradient step
|
64
|
+
#
|
65
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
66
|
+
# @since 0.3.1
|
67
|
+
#
|
68
|
+
# @api private
|
69
|
+
#
|
70
|
+
# @param start_color [Array<Integer>, String, Symbol] the starting color
|
71
|
+
# @param end_color [Array<Integer>, String, Symbol] the ending color
|
72
|
+
# @param step [Float] the interpolation step (0.0-1.0)
|
73
|
+
#
|
74
|
+
# @raise [ArgumentError] if step is not between 0.0 and 1.0
|
75
|
+
# @return [Array<Integer>] the interpolated RGB values
|
76
|
+
# @rbs (
|
77
|
+
# (Array[Integer] | String | Symbol) start_color,
|
78
|
+
# (Array[Integer] | String | Symbol) end_color,
|
79
|
+
# Float step
|
80
|
+
# ) -> Array[Integer]
|
81
|
+
def interpolate_color(start_color, end_color, step)
|
82
|
+
raise ArgumentError, "Invalid step: #{step}" unless step.between?(0.0, 1.0)
|
83
|
+
|
84
|
+
start_rgb = ColorSpace.resolve(start_color)
|
85
|
+
end_rgb = ColorSpace.resolve(end_color)
|
86
|
+
|
87
|
+
start_rgb.zip(end_rgb).map do |values|
|
88
|
+
start_val, end_val = values
|
89
|
+
next 0 unless start_val && end_val # Handle potential nil values
|
90
|
+
|
91
|
+
(start_val + ((end_val - start_val) * step)).round.clamp(0, 255)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Lighten an RGB color by a percentage
|
96
|
+
#
|
97
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
98
|
+
# @since 0.3.1
|
99
|
+
#
|
100
|
+
# @api private
|
101
|
+
#
|
102
|
+
# @param color [Array<Integer>, String, Symbol] the color to lighten
|
103
|
+
# @param amount [Float] amount to lighten by (0.0-1.0)
|
104
|
+
#
|
105
|
+
# @raise [ArgumentError] if amount is not between 0.0 and 1.0
|
106
|
+
# @return [Array<Integer>] the lightened RGB values
|
107
|
+
# @rbs ((Array[Integer] | String | Symbol) color, Float amount) -> Array[Integer]
|
108
|
+
def lighten(color, amount)
|
109
|
+
raise ArgumentError, "Invalid amount: #{amount}" unless amount.between?(0.0, 1.0)
|
110
|
+
|
111
|
+
rgb = ColorSpace.resolve(color)
|
112
|
+
rgb.map { |c| [255, (c * (1 + amount)).round].min }
|
113
|
+
end
|
114
|
+
|
115
|
+
# Generate a rainbow gradient with a specified number of steps
|
116
|
+
#
|
117
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
118
|
+
# @since 0.3.1
|
119
|
+
#
|
120
|
+
# @api private
|
121
|
+
#
|
122
|
+
# @param steps [Integer] the number of colors to generate (minimum 2)
|
123
|
+
#
|
124
|
+
# @raise [ArgumentError] if steps is less than 2
|
125
|
+
# @return [Array<Array<Integer>>] the rainbow gradient colors as RGB values
|
126
|
+
# @rbs (Integer steps) -> Array[Array[Integer]]
|
127
|
+
def rainbow_gradient(steps)
|
128
|
+
raise ArgumentError, "Steps must be at least 2, got: #{steps}" if steps < 2
|
129
|
+
|
130
|
+
hue_step = 360.0 / steps
|
131
|
+
(0...steps).map do |i|
|
132
|
+
hue = (i * hue_step) % 360
|
133
|
+
ColorSpace.hsv_to_rgb(hue, 1.0, 1.0)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|