inker 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,30 @@
1
+ require "yaml"
2
+ require "digest"
3
+ require "inker/version"
4
+ require "inker/color"
5
+ require "inker/wrappers/string"
6
+
7
+
8
+ # The main module of the gem, which loads all necessary classes and
9
+ # provides a helper for {Inker::Color} object generation from a string and
10
+ # for named colors map.
11
+ module Inker
12
+ class << self
13
+ # Creates a new instance of {Inker::Color}, which could be used for color
14
+ # manipulation or for collecting color info.
15
+ #
16
+ # @param str [String] the string to transform into {Inker::Color}
17
+ # @return [Inker::Color]
18
+ def color(str)
19
+ Color.new(str)
20
+ end
21
+
22
+
23
+ # Returns the map of named colors and their respective HEX representation.
24
+ #
25
+ # @return [Hash] a map of named colors and their HEX color
26
+ def named_colors
27
+ @named_colors ||= YAML.load_file(File.expand_path('../data/colors.yml', __FILE__))
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,152 @@
1
+ require_relative 'color/tools'
2
+ require_relative 'color/serializers'
3
+
4
+ module Inker
5
+ # This class is used to represent a color in Ruby as an object. It allows
6
+ # to create a new instance of {Inker::Color} from a string which represents a color.
7
+ # It also allows to obtain more info about the color and convert color to a different
8
+ # format.
9
+ class Color
10
+ extend Tools
11
+ include Serializers
12
+
13
+ attr_reader :red, :green, :blue, :alpha
14
+
15
+ # Create a new {Inker::Color} object from a color string.
16
+ #
17
+ # @param color_str [String] a color string
18
+ def initialize(color_str)
19
+ @input = color_str.to_s.downcase.gsub(/\s+/, "")
20
+
21
+ Color.parse_color(@input).tap do |color|
22
+ @red = color[:red]
23
+ @green = color[:green]
24
+ @blue = color[:blue]
25
+ @alpha = color[:alpha]
26
+ end
27
+
28
+ validate_color!
29
+ end
30
+
31
+ def ==(color)
32
+ self.red == color.red and
33
+ self.green == color.green and
34
+ self.blue == color.blue and
35
+ self.alpha == color.alpha
36
+ end
37
+
38
+
39
+ # Set the value of red component.
40
+ #
41
+ # @param value [Integer] the value of red component [0-255]
42
+ def red=(value)
43
+ raise ArgumentError.new("Invalid value: #{value.inspect}") if value.to_i < 0 or value.to_i > 255
44
+ @red = value
45
+ end
46
+
47
+
48
+ # Set the value of green component.
49
+ #
50
+ # @param value [Integer] the value of green component [0-255]
51
+ def green=(value)
52
+ raise ArgumentError.new("Invalid value: #{value.inspect}") if value.to_i < 0 or value.to_i > 255
53
+ @green = value
54
+ end
55
+
56
+
57
+ # Set the value of blue component.
58
+ #
59
+ # @param value [Integer] the value of blue component [0-255]
60
+ def blue=(value)
61
+ raise ArgumentError.new("Invalid value: #{value.inspect}") if value.to_i < 0 or value.to_i > 255
62
+ @blue = value
63
+ end
64
+
65
+
66
+ # Set the value of alpha component.
67
+ #
68
+ # @param value [Float] the value of alpha component [0.0-1.0]
69
+ def alpha=(value)
70
+ raise ArgumentError.new("Invalid value: #{value.inspect}") if value.to_f < 0 or value.to_f > 1
71
+ @alpha = value
72
+ end
73
+
74
+
75
+ # Calculate the brightness of a color.
76
+ #
77
+ # @return [Integer] a value between 0-255 which indicates the brightness of the color
78
+ def brightness
79
+ Color.brightness(@red, @green, @blue)
80
+ end
81
+
82
+
83
+ # Calculate the lightness of a color.
84
+ #
85
+ # @return [Float] a value between 0.0-1.0 which indicates the lightness of the color
86
+ def lightness
87
+ Color.lightness(@red, @green, @blue)
88
+ end
89
+
90
+
91
+ # Calculate the saturation of a color.
92
+ #
93
+ # @return [Float] a value between 0.0-1.0 which indicates the saturation of the color
94
+ def saturation
95
+ Color.saturation(@red, @green, @blue)
96
+ end
97
+
98
+
99
+ # Calculate the HUE of a color.
100
+ #
101
+ # @return [Integer] a value between 0-360 which indicates the HUE of the color
102
+ def hue
103
+ Color.hue(@red, @green, @blue)
104
+ end
105
+
106
+ # Returns a boolean which indicates if the color is dark.
107
+ #
108
+ # @return [Boolean] `true` when color is dark
109
+ def dark?
110
+ brightness < 128
111
+ end
112
+
113
+ # Returns a boolean which indicates if the color is light.
114
+ # @return [Boolean] `true` when color is light
115
+ def light?
116
+ !dark?
117
+ end
118
+
119
+
120
+ # Convert color to string in the specified format.
121
+ #
122
+ # @param format [String] indicates the format to which to output the color (default: `hex`)
123
+ #
124
+ # @return [String] a string representation of the color
125
+ def to_s(format = 'hex')
126
+ case format.to_s.strip.downcase
127
+ when 'hex6' then self.hex6
128
+ when 'rgb' then self.rgb
129
+ when 'rgba' then self.rgba
130
+ when 'hsl' then self.hsl
131
+ when 'hsla' then self.hsla
132
+ else
133
+ self.hex
134
+ end
135
+ end
136
+
137
+ private
138
+
139
+ # Validates the values of the color.
140
+ def validate_color!
141
+ invalid = (@red < 0 or @red > 255)
142
+ invalid ||= (@green < 0 or @green > 255)
143
+ invalid ||= (@blue < 0 or @blue > 255)
144
+ invalid ||= (@alpha < 0 or @alpha > 1)
145
+
146
+ if invalid
147
+ raise ArgumentError.new "Invalid color: #{@input.inspect} " \
148
+ "(R: #{@red.inspect}, G: #{@green.inspect}, B: #{@blue.inspect}, A: #{@alpha.inspect})"
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,69 @@
1
+ module Inker
2
+ class Color
3
+ # This module implements the methods which can be used to serialize
4
+ # a color as string in different formats.
5
+ module Serializers
6
+
7
+ # Convert color to a HEX color string.
8
+ #
9
+ # @param force_alpha [Boolean] indicates if alpha channel should be included in HEX color string
10
+ # when alpha component wasn't specified
11
+ # @return [String] a HEX color string
12
+ def hex(force_alpha: false)
13
+ result = hex6
14
+ result += (alpha * 255).to_i.to_s(16).rjust(2, "0") if alpha < 1 or force_alpha
15
+
16
+ return result
17
+ end
18
+
19
+
20
+ # Convert color to a HEX color string without alpha channel.
21
+ #
22
+ # @return [String] a HEX color string without alpha channel
23
+ def hex6
24
+ result = "#"
25
+ result += red.to_s(16).rjust(2, "0")
26
+ result += green.to_s(16).rjust(2, "0")
27
+ result += blue.to_s(16).rjust(2, "0")
28
+
29
+ return result
30
+ end
31
+
32
+
33
+ # Convert color to RGB color string.
34
+ #
35
+ # @return [String] a RGB color string
36
+ def rgb
37
+ return "rgb(#{red}, #{green}, #{blue})"
38
+ end
39
+
40
+
41
+ # Convert color to RGBA color string.
42
+ #
43
+ # @param alpha_precision [Integer] indicates the precision of alpha value
44
+ #
45
+ # @return [String] a RGBA color string
46
+ def rgba(alpha_precision: 2)
47
+ return "rgba(#{red}, #{green}, #{blue}, #{alpha.round(alpha_precision)})"
48
+ end
49
+
50
+
51
+ # Convert color to HSL color string.
52
+ #
53
+ # @return [String] a HSL color string
54
+ def hsl
55
+ return "hsl(#{hue}, #{(saturation * 100).round}%, #{(lightness * 100).round}%)"
56
+ end
57
+
58
+
59
+ # Convert color to HSL color string.
60
+ #
61
+ # @param alpha_precision [Integer] indicates the precision of alpha value
62
+ #
63
+ # @return [String] a HSL color string
64
+ def hsla(alpha_precision: 2)
65
+ return "hsl(#{hue}, #{(saturation * 100).round}%, #{(lightness * 100).round}%, #{alpha.round(alpha_precision)})"
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,389 @@
1
+ module Inker
2
+ class Color
3
+ # Tools module implements a set of methods useful for color parsing and
4
+ # for getting useful info about color.
5
+ module Tools
6
+ # Regular expression for HEX colors matching
7
+ HEX_REGEX = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/
8
+
9
+ # Regular expression for RGB colors matching
10
+ RGB_REGEX = /^rgb\((\d+,){2}\d+\)$/
11
+
12
+ # Regular expression for RGBA colors matching
13
+ RGBA_REGEX = /^rgba\((\d+,){3}\d+(.\d+)?\)$/
14
+
15
+ # Regular expression for HSL colors matching
16
+ HSL_REGEX = /^hsl\((\d+)(,\d+%|,\d+(.\d+)){2}\)$/
17
+
18
+ # Regular expression for HSLA colors matching
19
+ HSLA_REGEX = /^hsla\((\d+)(,\d+%|,\d+(.\d+)){2},\d+(.\d+)?\)$/
20
+
21
+ # Calculate the brightness of a color.
22
+ #
23
+ # @param red [Integer] the value of red component [0-255]
24
+ # @param green [Integer] the value of green component [0-255]
25
+ # @param blue [Integer] the value of blue component [0-255]
26
+ #
27
+ # @return [Integer] a value between 0-255 which indicates the brightness of the color
28
+ def brightness(red, green, blue)
29
+ Math.sqrt(0.299 * red**2 + 0.587 * green**2 + 0.114 * blue**2).round
30
+ end
31
+
32
+
33
+ # Calculate the lightness of a color from RGB components.
34
+ #
35
+ # @param red [Integer] the value of red component [0-255]
36
+ # @param green [Integer] the value of green component [0-255]
37
+ # @param blue [Integer] the value of blue component [0-255]
38
+ #
39
+ # @return [Float] a value in range 0.0-1.0 which indicates the ligthness of the color
40
+ def lightness(red, green, blue)
41
+ min, max = [red, green, blue].minmax
42
+
43
+ return (min + max) / (2.0 * 255)
44
+ end
45
+
46
+
47
+ # Calculate the saturation of a color from RGB components.
48
+ #
49
+ # @param red [Integer] the value of red component [0-255]
50
+ # @param green [Integer] the value of green component [0-255]
51
+ # @param blue [Integer] the value of blue component [0-255]
52
+ #
53
+ # @return [Float] a value in range 0.0-1.0 which indicates the saturation of the color
54
+ def saturation(red, green, blue)
55
+ # return 0 for black and white colors
56
+ return 0 if red == green and red == blue and (red == 0 or red == 255)
57
+
58
+ lightness = lightness(red, green, blue)
59
+ min, max = [red / 255.0, green / 255.0, blue / 255.0].minmax
60
+
61
+ return lightness < 0.5 ? (max - min) / (max + min) : (max - min) / (2.0 - max - min)
62
+ end
63
+
64
+
65
+ # Calculate the HUE value of a color from RGB components.
66
+ #
67
+ # @param red [Integer] the value of red component [0-255]
68
+ # @param green [Integer] the value of green component [0-255]
69
+ # @param blue [Integer] the value of blue component [0-255]
70
+ #
71
+ # @return [Integer] a value in range 0-360 which indicates the HUE value of the color
72
+ def hue(red, green, blue)
73
+ min, max = [red, green, blue].minmax
74
+
75
+ numerator = (max - min).to_f
76
+ return 0 if numerator == 0
77
+
78
+ hue = (red == max) ? (green - blue) / numerator :
79
+ (green == max) ? 2 + (blue - red) / numerator :
80
+ 4 + (red - green) / numerator
81
+ hue = hue * 60
82
+ return (hue < 0 ? hue + 360 : hue).round
83
+ end
84
+
85
+
86
+ # Get RGB values from a color in HSL format.
87
+ #
88
+ # @param hue [Integer] the value of HUE component [0-360]
89
+ # @param saturation [Float] the saturation of the color [0.0-1.0]
90
+ # @param lightness [Float] the lightness of the color [0.0-1.0]
91
+ #
92
+ # @return [Hash] a `Hash` which contains the values of RGB components
93
+ def hsl_to_rgb(hue, saturation, lightness)
94
+ result = nil
95
+ if saturation == 0
96
+ # There's no saturation, so it's a gray scale color, which
97
+ # depends only on brightness
98
+ brightness = (lightness * 255).round
99
+
100
+ # All RGB components are equal to brightness
101
+ result = {
102
+ red: brightness,
103
+ green: brightness,
104
+ blue: brightness
105
+ }
106
+ else
107
+ q = lightness < 0.5 ? lightness * (1 + saturation) : lightness + saturation - lightness * saturation
108
+ p = 2 * lightness - q
109
+ norm_hue = hue / 360.0
110
+
111
+ result = {
112
+ red: (hue_to_rgb(p, q, norm_hue + 1.0/3.0) * 255).round,
113
+ green: (hue_to_rgb(p, q, norm_hue) * 255).round,
114
+ blue: (hue_to_rgb(p, q, norm_hue - 1.0/3.0) * 255).round
115
+ }
116
+ end
117
+
118
+ return result
119
+ end
120
+
121
+
122
+ # Returns a `Boolean` which indicates if color is in HEX format
123
+ #
124
+ # @param color_str [String] a color string
125
+ #
126
+ # @return [Boolean] `true` when color is in HEX format
127
+ def is_hex?(color_str)
128
+ !!(color_str.to_s.downcase.strip =~ HEX_REGEX)
129
+ end
130
+
131
+
132
+ # Returns a `Boolean` which indicates if color is in RGB format
133
+ #
134
+ # @param color_str [String] a color string
135
+ #
136
+ # @return [Boolean] `true` when color is in RGB format
137
+ def is_rgb?(color_str)
138
+ !!(color_str.to_s.downcase.gsub(/\s+/, '') =~ RGB_REGEX)
139
+ end
140
+
141
+
142
+ # Returns a `Boolean` which indicates if color is in RGBA format
143
+ #
144
+ # @param color_str [String] a color string
145
+ #
146
+ # @return [Boolean] `true` when color is in RGBA format
147
+ def is_rgba?(color_str)
148
+ !!(color_str.to_s.downcase.gsub(/\s+/, '') =~ RGBA_REGEX)
149
+ end
150
+
151
+
152
+ # Returns a `Boolean` which indicates if color is in HSL format
153
+ #
154
+ # @param color_str [String] a color string
155
+ #
156
+ # @return [Boolean] `true` when color is in HSL format
157
+ def is_hsl?(color_str)
158
+ !!(color_str.to_s.downcase.gsub(/\s+/, '') =~ HSL_REGEX)
159
+ end
160
+
161
+
162
+ # Returns a `Boolean` which indicates if color is in HSLA format
163
+ #
164
+ # @param color_str [String] a color string
165
+ #
166
+ # @return [Boolean] `true` when color is in HSLA format
167
+ def is_hsla?(color_str)
168
+ !!(color_str.to_s.downcase.gsub(/\s+/, '') =~ HSLA_REGEX)
169
+ end
170
+
171
+
172
+ # Generate a random `Inker::Color`.
173
+ #
174
+ # @param with_alpha [Boolean] when `true` include alpha channel
175
+ #
176
+ # @return [Inker::Color] a random color
177
+ def random(with_alpha: false)
178
+ prefix = with_alpha ? "rgba" : "rgb"
179
+ values = (1..3).map{ (rand * 255).round }
180
+ values << rand.round(2) if with_alpha
181
+
182
+ Inker.color("#{prefix}(#{values.join(",")})")
183
+ end
184
+
185
+
186
+ # A helper for `Inker::Color` generation from RGB components.
187
+ #
188
+ # @param red [Integer] the value of red component [0-255]
189
+ # @param green [Integer] the value of green component [0-255]
190
+ # @param blue [Integer] the value of blue component [0-255]
191
+ #
192
+ # @return [Inker::Color] a `Inker::Color` generated from passed RGB values
193
+ def from_rgb(red, green, blue)
194
+ Inker.color("rgb(#{red}, #{green}, #{blue})")
195
+ end
196
+
197
+
198
+ # A helper for `Inker::Color` generation from RGBA components.
199
+ #
200
+ # @param red [Integer] the value of red component [0-255]
201
+ # @param green [Integer] the value of green component [0-255]
202
+ # @param blue [Integer] the value of blue component [0-255]
203
+ # @param alpha [Float] the value of alpha component [0.0-1.1]
204
+ #
205
+ # @return [Inker::Color] a `Inker::Color` generated from passed RGBA values
206
+ def from_rgba(red, green, blue, alpha)
207
+ Inker.color("rgba(#{red}, #{green}, #{blue}, #{alpha})")
208
+ end
209
+
210
+
211
+ # A helper for `Inker::Color` generation from HSL components.
212
+ #
213
+ # @param hue [Integer] the value of HUE component [0-360]
214
+ # @param saturation [Float] the value of saturation component [0.0-1.0]
215
+ # @param lightness [Float] the value of lightness component [0.0-1.0]
216
+ #
217
+ # @return [Inker::Color] a `Inker::Color` generated from passed HSL values
218
+ def from_hsl(hue, saturation, lightness)
219
+ Inker.color("hsl(#{hue}, #{saturation}, #{lightness})")
220
+ end
221
+
222
+
223
+ # A helper for `Inker::Color` generation from HSLA components.
224
+ #
225
+ # @param hue [Integer] the value of HUE component [0-360]
226
+ # @param saturation [Float] the value of saturation component [0.0-1.0]
227
+ # @param lightness [Float] the value of lightness component [0.0-1.0]
228
+ # @param alpha [Float] the value of alpha component [0.0-1.1]
229
+ #
230
+ # @return [Inker::Color] a `Inker::Color` generated from passed HSLA values
231
+ def from_hsla(hue, saturation, lightness, alpha)
232
+ Inker.color("hsla(#{hue}, #{saturation}, #{lightness}, #{alpha})")
233
+ end
234
+
235
+
236
+ # Use MD5 digest of the string to get hex values from specified positions (by default `[0, 29, 14, 30, 28, 31]`)
237
+ # in order to obtain a color in HEX format which represents the specified string.
238
+ #
239
+ # @params custom_string [String] a string from which to generate a color
240
+ # @params positions [Array] an array of 6 numbers in range 0-31 which indicates the position
241
+ # of hex value to get in order to obtain a 6 chars hex string, which will be the result color
242
+ #
243
+ # @return [Inker::Color] a `Inker::Color` object which represents the color associated to input string
244
+ def from_custom_string(custom_string, positions: [0, 29, 14, 30, 28, 31])
245
+ digest = Digest::MD5.hexdigest(custom_string.to_s)
246
+ Inker.color("##{positions.map{|p| digest[p]}.join}")
247
+ end
248
+
249
+
250
+ # Parse a color string an return it's RGBA components as a hash.
251
+ #
252
+ # @example
253
+ # Inker::Color.parse_color("#FF005544") # returns {:red=>255, :green=>0, :blue=>85, :alpha=>0.4}
254
+ #
255
+ # @param color_str [String] color string to parse
256
+ # @return [Hash] a hash which contains RGBA components of parsed color
257
+ def parse_color(color_str)
258
+ # Normalize input string by stripping white spaces and converting
259
+ # string to downcase
260
+ input = color_str.to_s.strip.downcase
261
+
262
+ # By default result is nil
263
+ result = nil
264
+
265
+ # Try to guess the format of color string and parse it by
266
+ # using the apropriate algorithm
267
+ if is_hex?(input)
268
+ # Parse the string as HEX color
269
+ result = parse_hex(input)
270
+ elsif is_rgb?(input)
271
+ # Parse the string as RGB color
272
+ result = parse_rgb(input)
273
+ elsif is_rgba?(input)
274
+ # Parse the string as RGBA color
275
+ result = parse_rgb(input, is_rgba: true)
276
+ elsif is_hsl?(input)
277
+ # Parse the string as HSL color
278
+ result = parse_hsl(input)
279
+ elsif is_hsla?(input)
280
+ # Parse the string as HSLA color
281
+ result = parse_hsl(input, is_hsla: true)
282
+ else
283
+ # Check if color is in "named color" format
284
+ named_color = Inker.named_colors[input]
285
+ if named_color
286
+ # If a named color has been matched, use it's HEX value and
287
+ # parse it as HEX color
288
+ result = parse_hex(named_color)
289
+ end
290
+ end
291
+
292
+ # If we didn't have any match, throw an ArgumentError error
293
+ raise ArgumentError.new("Unknown color format: #{color_str.to_s.strip.inspect}") if result.nil?
294
+
295
+ return result
296
+ end
297
+
298
+ private
299
+
300
+ # Parse a color string as HEX color.
301
+ #
302
+ # @param color_str [String] input color string
303
+ #
304
+ # @return [Hash] a `Hash` which contains RGBA components of parsed color
305
+ def parse_hex(color_str)
306
+ # Remove the leading '#' character from input color string
307
+ input = color_str.gsub(/^#/, '')
308
+
309
+ # Convert to HEX6 when color is in HEX3 format
310
+ input = input.chars.map{|x| x * 2 }.join if input.length == 3
311
+
312
+ # Get RGB components
313
+ result = {
314
+ red: Integer(input[0..1], 16),
315
+ green: Integer(input[2..3], 16),
316
+ blue: Integer(input[4..5], 16),
317
+ alpha: 1.0
318
+ }
319
+
320
+ # When color is in HEX8 format, get also alpha channel value
321
+ if input.length == 8
322
+ result[:alpha] = Integer(input[6..7], 16) / 255.0
323
+ end
324
+
325
+ return result
326
+ end
327
+
328
+
329
+ # Parse color string as RGB(A) color.
330
+ #
331
+ # @param color_str [String] input RGB(A) color string
332
+ # @param is_rgba [Boolean] indicates if color string is in RGBA format
333
+ #
334
+ # @return [Hash] a `Hash` which contains RGBA components of parsed color
335
+ def parse_rgb(color_str, is_rgba: false)
336
+ components = color_str.gsub(/(^rgb(a)?\(|\)$)/, "").split(",")
337
+
338
+ return {
339
+ red: components.shift.to_i,
340
+ green: components.shift.to_i,
341
+ blue: components.shift.to_i,
342
+ alpha: (is_rgba ? components.shift.to_f : 1.0)
343
+ }
344
+ end
345
+
346
+
347
+ # Parse color string as HSL(A) color.
348
+ #
349
+ # @param color_str [String] input HSL(A) color string
350
+ # @param is_hsla [Boolean] indicates if color string is in HSL(A) format
351
+ #
352
+ # @return [Hash] a `Hash` which contains RGBA components of parsed color
353
+ def parse_hsl(color_str, is_hsla: false)
354
+ components = color_str.gsub(/(^hsl(a)?\(|\)$)/, "").split(",")
355
+
356
+ hue = components.shift.to_i
357
+
358
+ saturation = components.shift
359
+ saturation = saturation.include?("%") ? saturation.to_f / 100 : saturation.to_f
360
+
361
+ lightness = components.shift
362
+ lightness = lightness.include?("%") ? lightness.to_f / 100 : lightness.to_f
363
+
364
+ result = hsl_to_rgb(hue, saturation, lightness)
365
+ result[:alpha] = is_hsla ? components.shift.to_f : 1.0
366
+
367
+ return result
368
+ end
369
+
370
+
371
+ # A helper function which allows to calculate the RGB component value from HSL color.
372
+ #
373
+ # @param p [Float] `2 * lightness -q`
374
+ # @param q [Float] `lightness < 0.5 ? lightness * (1 + saturation) : lightness + saturation - lightness * saturation`
375
+ # @param t [Float] `hue + 1/3`, `hue` or `hue - 1/3` according to which component is going to be calculated [r,g,b]
376
+ #
377
+ # @return [Float] a value which represents a RGB component in range 0.0-1.0
378
+ def hue_to_rgb(p, q, t)
379
+ t += 1 if t < 0
380
+ t -= 1 if t > 1
381
+
382
+ return p + (q - p) * 6 * t if t < 1.0 / 6.0
383
+ return q if t < 1.0 / 2.0
384
+ return p + (q - p) * (2/3 - t) * 6 if t < 2.0 / 3.0
385
+ return p;
386
+ end
387
+ end
388
+ end
389
+ end