sai 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -1
  3. data/README.md +11 -3
  4. data/docs/USAGE.md +57 -9
  5. data/lib/sai/ansi/color_parser.rb +109 -0
  6. data/lib/sai/ansi/sequence_processor.rb +15 -126
  7. data/lib/sai/ansi/style_parser.rb +66 -0
  8. data/lib/sai/ansi.rb +0 -27
  9. data/lib/sai/conversion/color_sequence.rb +4 -4
  10. data/lib/sai/conversion/rgb/color_classifier.rb +209 -0
  11. data/lib/sai/conversion/rgb/color_indexer.rb +48 -0
  12. data/lib/sai/conversion/rgb/color_space.rb +192 -0
  13. data/lib/sai/conversion/rgb/color_transformer.rb +140 -0
  14. data/lib/sai/conversion/rgb.rb +23 -269
  15. data/lib/sai/decorator/color_manipulations.rb +157 -0
  16. data/lib/sai/decorator/delegation.rb +84 -0
  17. data/lib/sai/decorator/gradients.rb +363 -0
  18. data/lib/sai/decorator/hex_colors.rb +56 -0
  19. data/lib/sai/decorator/named_colors.rb +780 -0
  20. data/lib/sai/decorator/named_styles.rb +276 -0
  21. data/lib/sai/decorator/rgb_colors.rb +64 -0
  22. data/lib/sai/decorator.rb +29 -775
  23. data/lib/sai/named_colors.rb +437 -0
  24. data/lib/sai.rb +731 -23
  25. data/sig/sai/ansi/color_parser.rbs +77 -0
  26. data/sig/sai/ansi/sequence_processor.rbs +0 -75
  27. data/sig/sai/ansi/style_parser.rbs +59 -0
  28. data/sig/sai/ansi.rbs +0 -10
  29. data/sig/sai/conversion/rgb/color_classifier.rbs +165 -0
  30. data/sig/sai/conversion/rgb/color_indexer.rbs +41 -0
  31. data/sig/sai/conversion/rgb/color_space.rbs +129 -0
  32. data/sig/sai/conversion/rgb/color_transformer.rbs +99 -0
  33. data/sig/sai/conversion/rgb.rbs +15 -198
  34. data/sig/sai/decorator/color_manipulations.rbs +125 -0
  35. data/sig/sai/decorator/delegation.rbs +47 -0
  36. data/sig/sai/decorator/gradients.rbs +267 -0
  37. data/sig/sai/decorator/hex_colors.rbs +48 -0
  38. data/sig/sai/decorator/named_colors.rbs +1491 -0
  39. data/sig/sai/decorator/named_styles.rbs +72 -0
  40. data/sig/sai/decorator/rgb_colors.rbs +52 -0
  41. data/sig/sai/decorator.rbs +21 -195
  42. data/sig/sai/named_colors.rbs +65 -0
  43. data/sig/sai.rbs +1468 -44
  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