pixelart-colors 0.1.0
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 +7 -0
- data/CHANGELOG.md +3 -0
- data/Manifest.txt +13 -0
- data/README.md +31 -0
- data/Rakefile +28 -0
- data/lib/pixelart/colors/base.rb +28 -0
- data/lib/pixelart/colors/color.rb +321 -0
- data/lib/pixelart/colors/format.rb +77 -0
- data/lib/pixelart/colors/gradient.rb +106 -0
- data/lib/pixelart/colors/palette.rb +72 -0
- data/lib/pixelart/colors/version.rb +25 -0
- data/lib/pixelart/colors.rb +22 -0
- data/sandbox/test_colors.rb +28 -0
- data/sandbox/test_gradient.rb +62 -0
- metadata +98 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 20d3ab72b943a3e9b6cbe0a7f67cf376f6f7a077de3c0456d93a8b2a3e386e0d
|
4
|
+
data.tar.gz: e4bdb8430d7d20337d3e01bd7daa885b01ead91f692af8d6af0a4b53b65c9f8d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7d8e890fd954aa76e116a0c9cf8ae27450b6115e0e41091dbcc580dd147cf609f21546acc8aa151b1df249f871e28aacc5173bbbee864e370a6a0e90d5aa8883
|
7
|
+
data.tar.gz: 5f83c8148e7ffb34f7b049481943ee671e3a6bd63bfbd38a43a2e82c6e23fff14e5d29b4823620b22da93474de12b0b8987fe82e38a3273b9b4b05e5a56f2c19
|
data/CHANGELOG.md
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
CHANGELOG.md
|
2
|
+
Manifest.txt
|
3
|
+
README.md
|
4
|
+
Rakefile
|
5
|
+
lib/pixelart/colors.rb
|
6
|
+
lib/pixelart/colors/base.rb
|
7
|
+
lib/pixelart/colors/color.rb
|
8
|
+
lib/pixelart/colors/format.rb
|
9
|
+
lib/pixelart/colors/gradient.rb
|
10
|
+
lib/pixelart/colors/palette.rb
|
11
|
+
lib/pixelart/colors/version.rb
|
12
|
+
sandbox/test_colors.rb
|
13
|
+
sandbox/test_gradient.rb
|
data/README.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# Pixel Art (True) Colors
|
2
|
+
|
3
|
+
pixelart-colors - helpers to work with true colors as integers; 256 shades for r/g/b (red, green and blue) totaling 16 million colors; incl. hsl, hsv color space converters and more
|
4
|
+
|
5
|
+
|
6
|
+
* home :: [github.com/pixelartexchange/pixelart](https://github.com/pixelartexchange/pixelart)
|
7
|
+
* bugs :: [github.com/pixelartexchange/pixelart/issues](https://github.com/pixelartexchange/pixelart/issues)
|
8
|
+
* gem :: [rubygems.org/gems/pixelart-colors](https://rubygems.org/gems/pixelart-colors)
|
9
|
+
* rdoc :: [rubydoc.info/gems/pixelart](http://rubydoc.info/gems/pixelart-colors)
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
To be done
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
## License
|
23
|
+
|
24
|
+
The scripts are dedicated to the public domain.
|
25
|
+
Use it as you please with no restrictions whatsoever.
|
26
|
+
|
27
|
+
|
28
|
+
## Questions? Comments?
|
29
|
+
|
30
|
+
Post them on the [D.I.Y. Punk (Pixel) Art reddit](https://old.reddit.com/r/DIYPunkArt). Thanks.
|
31
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'hoe'
|
2
|
+
require './lib/pixelart/colors/version.rb'
|
3
|
+
|
4
|
+
Hoe.spec 'pixelart-colors' do
|
5
|
+
|
6
|
+
self.version = Pixelart::Module::Colors::VERSION
|
7
|
+
|
8
|
+
|
9
|
+
self.summary = "pixelart-colors - helpers to work with true colors as integers; 256 shades for r/g/b (red, green and blue) totaling 16 million colors; incl. hsl, hsv color space converters and more"
|
10
|
+
self.description = summary
|
11
|
+
|
12
|
+
self.urls = { home: 'https://github.com/pixelartexchange/pixelart' }
|
13
|
+
|
14
|
+
self.author = 'Gerald Bauer'
|
15
|
+
self.email = 'wwwmake@googlegroups.com'
|
16
|
+
|
17
|
+
# switch extension to .markdown for gihub formatting
|
18
|
+
self.readme_file = 'README.md'
|
19
|
+
self.history_file = 'CHANGELOG.md'
|
20
|
+
|
21
|
+
self.extra_deps = []
|
22
|
+
|
23
|
+
self.licenses = ['Public Domain']
|
24
|
+
|
25
|
+
self.spec_extras = {
|
26
|
+
required_ruby_version: '>= 2.3'
|
27
|
+
}
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
|
2
|
+
# bonus / prologue / convenience 3rd party
|
3
|
+
# require 'csvreader' -- add later - why? why not?
|
4
|
+
|
5
|
+
|
6
|
+
## stdlib
|
7
|
+
require 'pp'
|
8
|
+
require 'time'
|
9
|
+
require 'date'
|
10
|
+
require 'fileutils'
|
11
|
+
|
12
|
+
require 'json'
|
13
|
+
require 'yaml'
|
14
|
+
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
## our own code
|
19
|
+
require 'pixelart/colors/version' # note: let version always go first
|
20
|
+
require 'pixelart/colors/color'
|
21
|
+
require 'pixelart/colors/format'
|
22
|
+
|
23
|
+
require 'pixelart/colors/gradient'
|
24
|
+
require 'pixelart/colors/palette'
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
puts Pixelart::Module::Colors.banner # say hello
|
@@ -0,0 +1,321 @@
|
|
1
|
+
module Pixelart
|
2
|
+
|
3
|
+
|
4
|
+
class Color ## todo/check - change class to module Color - why? why not?
|
5
|
+
|
6
|
+
TRANSPARENT = 0 # rgba( 0, 0, 0, 0)
|
7
|
+
BLACK = 0xff # rgba( 0, 0, 0, 255)
|
8
|
+
WHITE = 0xffffffff # rgba(255, 255, 255, 255)
|
9
|
+
|
10
|
+
|
11
|
+
def self.parse( color )
|
12
|
+
if color.is_a?( Integer ) ## e.g. assumes rgba or such
|
13
|
+
color ## pass through as is 1:1
|
14
|
+
elsif color.is_a?( Array ) ## assume array of hsl(a) e. g. [180, 0.86, 0.88]
|
15
|
+
from_hsl( *color )
|
16
|
+
elsif color.is_a?( String )
|
17
|
+
if color.downcase == 'transparent' ## special case for builtin colors
|
18
|
+
TRANSPARENT
|
19
|
+
else
|
20
|
+
## note: return an Integer !!! (not a Color class or such!!! )
|
21
|
+
from_hex( color )
|
22
|
+
end
|
23
|
+
else
|
24
|
+
raise ArgumentError, "unknown color format; cannot parse - expected rgb hex string e.g. d3d3d3"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
# Creates a new color using an r, g, b triple and an alpha value.
|
30
|
+
# @param [Integer] r The r-component (0-255)
|
31
|
+
# @param [Integer] g The g-component (0-255)
|
32
|
+
# @param [Integer] b The b-component (0-255)
|
33
|
+
# @param [Integer] a The opacity (0-255)
|
34
|
+
# @return [Integer] The newly constructed color value.
|
35
|
+
def self.rgba(r, g, b, a)
|
36
|
+
r << 24 | g << 16 | b << 8 | a
|
37
|
+
end
|
38
|
+
|
39
|
+
# Creates a new color using an r, g, b triple.
|
40
|
+
# @param [Integer] r The r-component (0-255)
|
41
|
+
# @param [Integer] g The g-component (0-255)
|
42
|
+
# @param [Integer] b The b-component (0-255)
|
43
|
+
# @return [Integer] The newly constructed color value.
|
44
|
+
def self.rgb(r, g, b)
|
45
|
+
r << 24 | g << 16 | b << 8 | 0xff
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
# Returns the red-component from the color value.
|
50
|
+
#
|
51
|
+
# @param [Integer] value The color value.
|
52
|
+
# @return [Integer] A value between 0 and MAX.
|
53
|
+
def self.r(value)
|
54
|
+
(value & 0xff000000) >> 24
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns the green-component from the color value.
|
58
|
+
#
|
59
|
+
# @param [Integer] value The color value.
|
60
|
+
# @return [Integer] A value between 0 and MAX.
|
61
|
+
def self.g(value)
|
62
|
+
(value & 0x00ff0000) >> 16
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns the blue-component from the color value.
|
66
|
+
#
|
67
|
+
# @param [Integer] value The color value.
|
68
|
+
# @return [Integer] A value between 0 and MAX.
|
69
|
+
def self.b(value)
|
70
|
+
(value & 0x0000ff00) >> 8
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns the alpha channel value for the color value.
|
74
|
+
#
|
75
|
+
# @param [Integer] value The color value.
|
76
|
+
# @return [Integer] A value between 0 and MAX.
|
77
|
+
def self.a(value)
|
78
|
+
value & 0x000000ff
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
|
83
|
+
|
84
|
+
# Returns true if this color is fully opaque.
|
85
|
+
#
|
86
|
+
# @param [Integer] value The color to test.
|
87
|
+
# @return [true, false] True if the alpha channel equals MAX.
|
88
|
+
def self.opaque?(value)
|
89
|
+
a(value) == 0x000000ff
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns true if this color is fully transparent.
|
93
|
+
#
|
94
|
+
# @param [Integer] value The color to test.
|
95
|
+
# @return [true, false] True if the r, g and b component are equal.
|
96
|
+
def self.grayscale?(value)
|
97
|
+
r(value) == b(value) && b(value) == g(value)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns true if this color is fully transparent.
|
101
|
+
#
|
102
|
+
# @param [Integer] value The color to test.
|
103
|
+
# @return [true, false] True if the alpha channel equals 0.
|
104
|
+
def self.transparent?(value)
|
105
|
+
a(value) == 0x00000000
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
|
110
|
+
# The regexp to parse 3-digit hex color values.
|
111
|
+
HEX3_COLOR_REGEXP = /\A(?:#|0x)?([0-9a-f]{3})\z/i
|
112
|
+
|
113
|
+
# The regexp to parse 6- and 8-digit hex color values.
|
114
|
+
HEX6_COLOR_REGEXP = /\A(?:#|0x)?([0-9a-f]{6})([0-9a-f]{2})?\z/i
|
115
|
+
|
116
|
+
|
117
|
+
def self.from_hex( hex_str )
|
118
|
+
## Creates a color by converting it from a string in hex notation.
|
119
|
+
##
|
120
|
+
## It supports colors with (#rrggbbaa) or without (#rrggbb)
|
121
|
+
## alpha channel as well as the 3-digit short format (#rgb)
|
122
|
+
## for those without. Color strings may include
|
123
|
+
## the prefix "0x" or "#"".
|
124
|
+
base_color = case hex_str
|
125
|
+
when HEX3_COLOR_REGEXP
|
126
|
+
$1.gsub( /([0-9a-f])/i, '\1\1' ).hex << 8
|
127
|
+
when HEX6_COLOR_REGEXP
|
128
|
+
$1.hex << 8
|
129
|
+
else
|
130
|
+
raise ArgumentError, "Not a valid hex color notation: #{hex_str.inspect}!"
|
131
|
+
end
|
132
|
+
opacity = $2 ? $2.hex : 0xff
|
133
|
+
base_color | opacity
|
134
|
+
end
|
135
|
+
|
136
|
+
# Returns a string representing this color using hex notation (i.e.
|
137
|
+
# #rrggbbaa).
|
138
|
+
#
|
139
|
+
# @param [Integer] color The color to convert.
|
140
|
+
# @param [Boolean] include_alpha
|
141
|
+
# @return [String] The color in hex notation, starting with a pound sign.
|
142
|
+
def self.to_hex( color, include_alpha: true )
|
143
|
+
include_alpha ? ("#%08x" % color) : ("#%06x" % [color >> 8])
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
|
148
|
+
# Creates a new color from an HSL triple.
|
149
|
+
#
|
150
|
+
# This implementation follows the modern convention of 0 degrees hue
|
151
|
+
# indicating red.
|
152
|
+
#
|
153
|
+
# @param [Fixnum] hue The hue component (0-360)
|
154
|
+
# @param [Fixnum] saturation The saturation component (0-1)
|
155
|
+
# @param [Fixnum] lightness The lightness component (0-1)
|
156
|
+
# @param [Fixnum] alpha Defaults to opaque (255).
|
157
|
+
# @return [Integer] The newly constructed color value.
|
158
|
+
# @raise [ArgumentError] if the hsl triple is invalid.
|
159
|
+
# @see https://en.wikipedia.org/wiki/HSL_and_HSV
|
160
|
+
|
161
|
+
def self.from_hsl( hue, saturation, lightness, alpha=255)
|
162
|
+
raise ArgumentError, "Hue #{hue} was not between 0 and 360" unless (0..360).cover?(hue)
|
163
|
+
raise ArgumentError, "Saturation #{saturation} was not between 0 and 1" unless (0..1).cover?(saturation)
|
164
|
+
raise ArgumentError, "Lightness #{lightness} was not between 0 and 1" unless (0..1).cover?(lightness)
|
165
|
+
|
166
|
+
chroma = (1 - (2 * lightness - 1).abs) * saturation
|
167
|
+
rgb = cylindrical_to_cubic(hue, saturation, lightness, chroma)
|
168
|
+
rgb.map! { |component| ((component + lightness - 0.5 * chroma) * 255).to_i }
|
169
|
+
rgb << alpha
|
170
|
+
rgba(*rgb)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Creates a new color from an HSV triple.
|
174
|
+
#
|
175
|
+
# Create a new color using an HSV (sometimes also called HSB) triple. The
|
176
|
+
# words `value` and `brightness` are used interchangeably and synonymously
|
177
|
+
# in descriptions of this colorspace. This implementation follows the modern
|
178
|
+
# convention of 0 degrees hue indicating red.
|
179
|
+
#
|
180
|
+
# @param [Fixnum] hue The hue component (0-360)
|
181
|
+
# @param [Fixnum] saturation The saturation component (0-1)
|
182
|
+
# @param [Fixnum] value The value (brightness) component (0-1)
|
183
|
+
# @param [Fixnum] alpha Defaults to opaque (255).
|
184
|
+
# @return [Integer] The newly constructed color value.
|
185
|
+
# @raise [ArgumentError] if the hsv triple is invalid.
|
186
|
+
# @see https://en.wikipedia.org/wiki/HSL_and_HSV
|
187
|
+
def self.from_hsv(hue, saturation, value, alpha=255)
|
188
|
+
raise ArgumentError, "Hue must be between 0 and 360" unless (0..360).cover?(hue)
|
189
|
+
raise ArgumentError, "Saturation must be between 0 and 1" unless (0..1).cover?(saturation)
|
190
|
+
raise ArgumentError, "Value/brightness must be between 0 and 1" unless (0..1).cover?(value)
|
191
|
+
|
192
|
+
chroma = value * saturation
|
193
|
+
rgb = cylindrical_to_cubic(hue, saturation, value, chroma)
|
194
|
+
rgb.map! { |component| ((component + value - chroma) * 255).to_i }
|
195
|
+
rgb << alpha
|
196
|
+
rgba(*rgb)
|
197
|
+
end
|
198
|
+
|
199
|
+
# Convert one HSL or HSV triple and associated chroma to a scaled rgb triple
|
200
|
+
#
|
201
|
+
# This method encapsulates the shared mathematical operations needed to
|
202
|
+
# convert coordinates from a cylindrical colorspace such as HSL or HSV into
|
203
|
+
# coordinates of the RGB colorspace.
|
204
|
+
#
|
205
|
+
# Even though chroma values are derived from the other three coordinates,
|
206
|
+
# the formula for calculating chroma differs for each colorspace. Since it
|
207
|
+
# is calculated differently for each colorspace, it must be passed in as
|
208
|
+
# a parameter.
|
209
|
+
#
|
210
|
+
# @param [Fixnum] hue The hue-component (0-360)
|
211
|
+
# @param [Fixnum] saturation The saturation-component (0-1)
|
212
|
+
# @param [Fixnum] y_component The y_component can represent either lightness
|
213
|
+
# or brightness/value (0-1) depending on which scheme (HSV/HSL) is being used.
|
214
|
+
# @param [Fixnum] chroma The associated chroma value.
|
215
|
+
# @return [Array<Fixnum>] A scaled r,g,b triple. Scheme-dependent
|
216
|
+
# adjustments are still needed to reach the true r,g,b values.
|
217
|
+
# @see https://en.wikipedia.org/wiki/HSL_and_HSV
|
218
|
+
def self.cylindrical_to_cubic(hue, saturation, y_component, chroma)
|
219
|
+
hue_prime = hue.fdiv(60)
|
220
|
+
x = chroma * (1 - (hue_prime % 2 - 1).abs)
|
221
|
+
|
222
|
+
case hue_prime
|
223
|
+
when (0...1) then [chroma, x, 0]
|
224
|
+
when (1...2) then [x, chroma, 0]
|
225
|
+
when (2...3) then [0, chroma, x]
|
226
|
+
when (3...4) then [0, x, chroma]
|
227
|
+
when (4...5) then [x, 0, chroma]
|
228
|
+
when (5..6) then [chroma, 0, x]
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
|
233
|
+
|
234
|
+
# Returns an array with the separate HSL components of a color.
|
235
|
+
#
|
236
|
+
# Note: Because this code internally handles colors as Integers for performance
|
237
|
+
# reasons, some rounding occurs when importing or exporting HSL colors
|
238
|
+
# whose coordinates are float-based. Because of this rounding, #to_hsl and
|
239
|
+
# #from_hsl may not be perfect inverses.
|
240
|
+
#
|
241
|
+
# This implementation follows the modern convention of 0 degrees hue indicating red.
|
242
|
+
#
|
243
|
+
# @param [Integer] color The color to convert.
|
244
|
+
# @param [Boolean] include_alpha Flag indicates whether a fourth element
|
245
|
+
# representing alpha channel should be included in the returned array.
|
246
|
+
# @return [Array<Fixnum>[0]] The hue of the color (0-360)
|
247
|
+
# @return [Array<Fixnum>[1]] The saturation of the color (0-1)
|
248
|
+
# @return [Array<Fixnum>[2]] The lightness of the color (0-1)
|
249
|
+
# @return [Array<Fixnum>[3]] Optional fourth element for alpha, included if
|
250
|
+
# include_alpha=true (0-255)
|
251
|
+
# @see https://en.wikipedia.org/wiki/HSL_and_HSV
|
252
|
+
def self.to_hsl(color, include_alpha: true )
|
253
|
+
hue, chroma, max, min = hue_and_chroma(color)
|
254
|
+
lightness = 0.5 * (max + min)
|
255
|
+
saturation = chroma.zero? ? 0.0 : chroma.fdiv(1 - (2 * lightness - 1).abs)
|
256
|
+
|
257
|
+
include_alpha ? [hue, saturation, lightness, a(color)] :
|
258
|
+
[hue, saturation, lightness]
|
259
|
+
end
|
260
|
+
|
261
|
+
# Returns an array with the separate HSV components of a color.
|
262
|
+
#
|
263
|
+
# Note: Because this code internally handles colors as Integers for performance
|
264
|
+
# reasons, some rounding occurs when importing or exporting HSV colors
|
265
|
+
# whose coordinates are float-based. Because of this rounding, #to_hsv and
|
266
|
+
# #from_hsv may not be perfect inverses.
|
267
|
+
#
|
268
|
+
# This implementation follows the modern convention of 0 degrees hue
|
269
|
+
# indicating red.
|
270
|
+
#
|
271
|
+
# @param [Integer] color
|
272
|
+
# @param [Boolean] include_alpha Flag indicates whether a fourth element
|
273
|
+
# representing alpha channel should be included in the returned array.
|
274
|
+
# @return [Array[0]] The hue of the color (0-360)
|
275
|
+
# @return [Array[1]] The saturation of the color (0-1)
|
276
|
+
# @return [Array[2]] The value of the color (0-1)
|
277
|
+
# @return [Array[3]] Optional fourth element for alpha, included if
|
278
|
+
# include_alpha=true (0-255)
|
279
|
+
# @see https://en.wikipedia.org/wiki/HSL_and_HSV
|
280
|
+
def self.to_hsv(color, include_alpha: true )
|
281
|
+
hue, chroma, max, _ = hue_and_chroma(color)
|
282
|
+
value = max
|
283
|
+
saturation = chroma.zero? ? 0.0 : chroma.fdiv(value)
|
284
|
+
|
285
|
+
include_alpha ? [hue, saturation, value, a(color)] :
|
286
|
+
[hue, saturation, value]
|
287
|
+
end
|
288
|
+
|
289
|
+
|
290
|
+
|
291
|
+
# This method encapsulates the logic needed to extract hue and chroma from
|
292
|
+
# a color. This logic is shared by the cylindrical HSV/HSB and HSL
|
293
|
+
# color space models.
|
294
|
+
#
|
295
|
+
# @param [Integer] color.
|
296
|
+
# @return [Fixnum] hue The hue of the color (0-360)
|
297
|
+
# @return [Fixnum] chroma The chroma of the color (0-1)
|
298
|
+
# @return [Fixnum] max The magnitude of the largest scaled rgb component (0-1)
|
299
|
+
# @return [Fixnum] min The magnitude of the smallest scaled rgb component (0-1)
|
300
|
+
# @private
|
301
|
+
def self.hue_and_chroma(color)
|
302
|
+
scaled_rgb = [r(color), g(color), b(color)]
|
303
|
+
scaled_rgb.map! { |component| component.fdiv(255) }
|
304
|
+
min, max = scaled_rgb.minmax
|
305
|
+
chroma = max - min
|
306
|
+
|
307
|
+
r, g, b = scaled_rgb
|
308
|
+
hue_prime = chroma.zero? ? 0 : case max
|
309
|
+
when r then (g - b).fdiv(chroma)
|
310
|
+
when g then (b - r).fdiv(chroma) + 2
|
311
|
+
when b then (r - g).fdiv(chroma) + 4
|
312
|
+
else 0
|
313
|
+
end
|
314
|
+
hue = 60 * hue_prime
|
315
|
+
|
316
|
+
[hue.round, chroma, max, min]
|
317
|
+
end
|
318
|
+
end # class Color
|
319
|
+
end # module Pixelart
|
320
|
+
|
321
|
+
|
@@ -0,0 +1,77 @@
|
|
1
|
+
|
2
|
+
module Pixelart
|
3
|
+
class Color ## todo/check - change class to module Color - why? why not?
|
4
|
+
|
5
|
+
|
6
|
+
## known built-in color names
|
7
|
+
def self.build_names
|
8
|
+
names = {
|
9
|
+
'#00000000' => 'TRANSPARENT',
|
10
|
+
'#000000ff' => 'BLACK',
|
11
|
+
'#ffffffff' => 'WHITE',
|
12
|
+
}
|
13
|
+
|
14
|
+
## auto-add grayscale 1 to 254
|
15
|
+
(1..254).each do |n|
|
16
|
+
hex = "#" + ('%02x' % n)*3
|
17
|
+
hex << "ff" ## add alpha channel (255)
|
18
|
+
names[ hex ] = "8-BIT GRAYSCALE ##{n}"
|
19
|
+
end
|
20
|
+
|
21
|
+
names
|
22
|
+
end
|
23
|
+
|
24
|
+
NAMES = build_names
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
def self.format( color )
|
29
|
+
rgb = [r(color),
|
30
|
+
g(color),
|
31
|
+
b(color)]
|
32
|
+
|
33
|
+
# rgb in hex (string format)
|
34
|
+
# note: do NOT include alpha channel for now - why? why not?
|
35
|
+
hex = "#" + rgb.map{|num| '%02x' % num }.join
|
36
|
+
|
37
|
+
hsl = to_hsl( color )
|
38
|
+
hsv = to_hsv( color )
|
39
|
+
|
40
|
+
buf = ''
|
41
|
+
buf << hex
|
42
|
+
buf << " / "
|
43
|
+
buf << "rgb("
|
44
|
+
buf << "%3d " % rgb[0]
|
45
|
+
buf << "%3d " % rgb[1]
|
46
|
+
buf << "%3d)" % rgb[2]
|
47
|
+
buf << " - "
|
48
|
+
buf << "hsl("
|
49
|
+
buf << "%3d° " % (hsl[0] % 360)
|
50
|
+
buf << "%3d%% " % (hsl[1]*100+0.5).to_i
|
51
|
+
buf << "%3d%%)" % (hsl[2]*100+0.5).to_i
|
52
|
+
buf << " - "
|
53
|
+
buf << "hsv("
|
54
|
+
buf << "%3d° " % (hsv[0] % 360)
|
55
|
+
buf << "%3d%% " % (hsv[1]*100+0.5).to_i
|
56
|
+
buf << "%3d%%)" % (hsv[2]*100+0.5).to_i
|
57
|
+
|
58
|
+
alpha = a(color)
|
59
|
+
if alpha != 255
|
60
|
+
buf << " - α(%3d%%)" % (alpha*100/255+0.5).to_i
|
61
|
+
else
|
62
|
+
buf << " " ## add empty for 255 (full opacity)
|
63
|
+
end
|
64
|
+
|
65
|
+
## note: add alpha channel to hex
|
66
|
+
alpha_hex = '%02x' % alpha
|
67
|
+
name = NAMES[ hex+alpha_hex ]
|
68
|
+
buf << " - #{name}" if name
|
69
|
+
|
70
|
+
buf
|
71
|
+
end
|
72
|
+
class << self
|
73
|
+
alias_method :fmt, :format
|
74
|
+
end
|
75
|
+
|
76
|
+
end # class Color
|
77
|
+
end # module Pixelart
|
@@ -0,0 +1,106 @@
|
|
1
|
+
|
2
|
+
## inspired / helped by
|
3
|
+
## https://en.wikipedia.org/wiki/List_of_software_palettes#Color_gradient_palettes
|
4
|
+
## https://github.com/mistic100/tinygradient
|
5
|
+
## https://mistic100.github.io/tinygradient/
|
6
|
+
## https://bsouthga.dev/posts/color-gradients-with-python
|
7
|
+
|
8
|
+
|
9
|
+
module Pixelart
|
10
|
+
|
11
|
+
class Gradient
|
12
|
+
|
13
|
+
def initialize( *stops )
|
14
|
+
## note: convert stop colors to rgb triplets e.g.
|
15
|
+
## from #ffffff to [255,255,255]
|
16
|
+
## #000000 to [0,0,0] etc.
|
17
|
+
@stops = stops.map do |stop|
|
18
|
+
stop = Color.parse( stop )
|
19
|
+
[Color.r(stop), Color.g(stop), Color.b(stop)]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def colors( steps )
|
25
|
+
segments = @stops.size - 1
|
26
|
+
|
27
|
+
## note: gradient will include start (first)
|
28
|
+
## AND stop color (last) - stop color is NOT excluded for now!!
|
29
|
+
if segments == 1
|
30
|
+
start = @stops[0]
|
31
|
+
stop = @stops[1]
|
32
|
+
|
33
|
+
gradient = linear_gradient( start, stop, steps,
|
34
|
+
include_stop: true )
|
35
|
+
else
|
36
|
+
steps_per_segment, mod = steps.divmod( segments )
|
37
|
+
raise ArgumentError, "steps (#{steps}) must be divisible by # of segments (#{segments}); expected mod of 0 but got #{mod}" if mod != 0
|
38
|
+
|
39
|
+
gradient = []
|
40
|
+
segments.times do |segment|
|
41
|
+
start = @stops[segment]
|
42
|
+
stop = @stops[segment+1]
|
43
|
+
include_stop = (segment == segments-1) ## note: only include stop if last segment!!
|
44
|
+
|
45
|
+
# print " segment #{segment+1}/#{segments} #{steps_per_segment} color(s) - "
|
46
|
+
# print " start: #{start.inspect} "
|
47
|
+
# print include_stop ? 'include' : 'exclude'
|
48
|
+
# print " stop: #{stop.inspect}"
|
49
|
+
# print "\n"
|
50
|
+
|
51
|
+
gradient += linear_gradient( start, stop, steps_per_segment,
|
52
|
+
include_stop: include_stop )
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
## convert to color (Integer)
|
57
|
+
gradient.map do |color|
|
58
|
+
Color.rgb( *color )
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
|
64
|
+
def interpolate( start, stop, steps, n )
|
65
|
+
## note: n - expected to start with 1,2,3,etc.
|
66
|
+
color = []
|
67
|
+
3.times do |i|
|
68
|
+
stepize = Float(stop[i] - start[i]) / Float(steps-1)
|
69
|
+
value = stepize * n
|
70
|
+
## convert back to Integer from Float
|
71
|
+
## add 0.5 for rounding up (starting with 0.5) - why? why not?
|
72
|
+
value = (value+0.5).to_i
|
73
|
+
value = start[i] + value
|
74
|
+
|
75
|
+
color << value
|
76
|
+
end
|
77
|
+
color
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
def linear_gradient( start, stop, steps,
|
82
|
+
include_stop: true )
|
83
|
+
|
84
|
+
gradient = [start] ## auto-add start color (first color in gradient)
|
85
|
+
|
86
|
+
if include_stop
|
87
|
+
1.upto( steps-2 ).each do |n| ## sub two (-2), that is, start and stop color
|
88
|
+
gradient << interpolate( start, stop, steps, n )
|
89
|
+
end
|
90
|
+
# note: use original passed in stop color (should match calculated)
|
91
|
+
gradient << stop
|
92
|
+
else
|
93
|
+
1.upto( steps-1 ).each do |n| ## sub one (-1), that is, start color only
|
94
|
+
## note: add one (+1) to steps because stop color gets excluded (not included)!!
|
95
|
+
gradient << interpolate( start, stop, steps+1, n )
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
gradient
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
|
104
|
+
end # class Gradient
|
105
|
+
end # module Pixelart
|
106
|
+
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Pixelart
|
2
|
+
|
3
|
+
|
4
|
+
class Palette8bit # or use Palette256 alias?
|
5
|
+
## todo/check: change class Palette8Bit to Module (like Class) - why? why not?
|
6
|
+
|
7
|
+
## auto-add grayscale 0 to 255
|
8
|
+
## e.g. rgb(0,0,0)
|
9
|
+
## rgb(1,1,1)
|
10
|
+
## rgb(2,2,2)
|
11
|
+
## ...
|
12
|
+
## rgb(255,255,255)
|
13
|
+
GRAYSCALE = (0..255).map { |n| Color.rgb( n, n, n ) }
|
14
|
+
|
15
|
+
|
16
|
+
## 8x32 gradient color stops
|
17
|
+
## see https://en.wikipedia.org/wiki/List_of_software_palettes#Color_gradient_palettes
|
18
|
+
|
19
|
+
SEPIA_STOPS = [
|
20
|
+
['080400', '262117'],
|
21
|
+
['272218', '453E2F'],
|
22
|
+
['463F30', '645C48'],
|
23
|
+
['655D48', '837A60'],
|
24
|
+
|
25
|
+
['847A60', 'A29778'],
|
26
|
+
['A39878', 'C1B590'],
|
27
|
+
['C2B691', 'E0D2A8'],
|
28
|
+
['E1D3A9', 'FEEFBF'],
|
29
|
+
]
|
30
|
+
|
31
|
+
BLUE_STOPS = [
|
32
|
+
['000000', '001F3E'],
|
33
|
+
['002040', '003F7E'],
|
34
|
+
['004080', '005FBD'],
|
35
|
+
['0060BF', '007FFD'],
|
36
|
+
|
37
|
+
['0080FF', '009FFF'],
|
38
|
+
['00A0FF', '00BFFF'],
|
39
|
+
['00C0FF', '00DFFF'],
|
40
|
+
['00E0FF', '00FEFF'],
|
41
|
+
]
|
42
|
+
|
43
|
+
FALSE_STOPS = [
|
44
|
+
['FF00FF', '6400FF'],
|
45
|
+
['5F00FF', '003CFF'],
|
46
|
+
['0041FF', '00DCFF'],
|
47
|
+
['00E1FF', '00FF82'],
|
48
|
+
|
49
|
+
['00FF7D', '1EFF00'],
|
50
|
+
['23FF00', 'BEFF00'],
|
51
|
+
['C3FF00', 'FFA000'],
|
52
|
+
['FF9B00', 'FF0000'],
|
53
|
+
]
|
54
|
+
|
55
|
+
|
56
|
+
def self.build_palette( gradients )
|
57
|
+
colors_per_gradient, mod = 256.divmod( gradients.size )
|
58
|
+
raise ArgumentError, "8bit palette - 256 must be divisible by # of gradients (#{gradients.size}; expected mod of 0 but got #{mod}" if mod != 0
|
59
|
+
|
60
|
+
colors = []
|
61
|
+
gradients.each do |stops|
|
62
|
+
colors += Gradient.new( *stops ).colors( colors_per_gradient )
|
63
|
+
end
|
64
|
+
colors
|
65
|
+
end
|
66
|
+
|
67
|
+
SEPIA = build_palette( SEPIA_STOPS )
|
68
|
+
BLUE = build_palette( BLUE_STOPS )
|
69
|
+
FALSE = build_palette( FALSE_STOPS )
|
70
|
+
end # class Palette8bit
|
71
|
+
end # module Pixelart
|
72
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
|
2
|
+
module Pixelart
|
3
|
+
module Module
|
4
|
+
module Colors
|
5
|
+
MAJOR = 0
|
6
|
+
MINOR = 1
|
7
|
+
PATCH = 0
|
8
|
+
VERSION = [MAJOR,MINOR,PATCH].join('.')
|
9
|
+
|
10
|
+
def self.version
|
11
|
+
VERSION
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.banner
|
15
|
+
"pixelart-colors/#{VERSION} on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}] in (#{root})"
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.root
|
19
|
+
File.expand_path( File.dirname(File.dirname(File.dirname(File.dirname(__FILE__)))) )
|
20
|
+
end
|
21
|
+
|
22
|
+
end # module Colors
|
23
|
+
end # module Module
|
24
|
+
end # module Pixelart
|
25
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
## our own code (without "top-level" shortcuts e.g. "modular version")
|
3
|
+
require 'pixelart/colors/base' # aka "strict(er)" version
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
##########
|
8
|
+
# add some spelling convenience variants
|
9
|
+
PixelArt = Pixelart
|
10
|
+
|
11
|
+
module Pixelart
|
12
|
+
Palette256 = Palette8Bit = Palette8bit
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
|
17
|
+
###
|
18
|
+
# add convenience top-level shortcuts / aliases
|
19
|
+
# make Image, Color, Palette8bit, etc top-level
|
20
|
+
include Pixelart
|
21
|
+
|
22
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
###
|
2
|
+
# to run use
|
3
|
+
# ruby -I ./lib sandbox/test_colors.rb
|
4
|
+
|
5
|
+
|
6
|
+
require 'pixelart/colors/base'
|
7
|
+
|
8
|
+
colors = {'transparent' => Pixelart::Color::TRANSPARENT,
|
9
|
+
'black' => Pixelart::Color::BLACK,
|
10
|
+
'white' => Pixelart::Color::WHITE,
|
11
|
+
'red' => 0xff0000ff,
|
12
|
+
'yellow' => 0xffff00ff, # r(ed) + g(reen) = yellow
|
13
|
+
}
|
14
|
+
|
15
|
+
|
16
|
+
colors.each do |name,color|
|
17
|
+
puts "==> #{name} - #{color}"
|
18
|
+
print " transparent? "; pp Pixelart::Color.transparent?( color )
|
19
|
+
print " opaque? "; pp Pixelart::Color.opaque?( color )
|
20
|
+
|
21
|
+
print " to_hex: "; pp Pixelart::Color.to_hex( color )
|
22
|
+
print " to_hsl: "; pp Pixelart::Color.to_hsl( color )
|
23
|
+
print " to_hsv: "; pp Pixelart::Color.to_hsv( color )
|
24
|
+
print " "; puts Pixelart::Color.format( color )
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
puts "bye"
|
@@ -0,0 +1,62 @@
|
|
1
|
+
###
|
2
|
+
# to run use
|
3
|
+
# ruby -I ./lib sandbox/test_gradient.rb
|
4
|
+
|
5
|
+
|
6
|
+
require 'pixelart/colors/base'
|
7
|
+
|
8
|
+
|
9
|
+
gradient = Pixelart::Gradient.new( '000000', 'ffffff' )
|
10
|
+
# gradient = Pixelart::Gradient.new( 'ffffff', '000000' )
|
11
|
+
|
12
|
+
pp colors = gradient.colors( 256 ) ## 256 steps
|
13
|
+
puts "---"
|
14
|
+
pp colors.map { |color| Pixelart::Color.format( color ) }
|
15
|
+
|
16
|
+
puts
|
17
|
+
puts "---"
|
18
|
+
pp colors = gradient.colors( 10 ) ## 10 steps
|
19
|
+
puts "---"
|
20
|
+
pp colors.map { |color| Pixelart::Color.format( color ) }
|
21
|
+
|
22
|
+
|
23
|
+
|
24
|
+
gradient = Pixelart::Gradient.new( '000000',
|
25
|
+
'050505',
|
26
|
+
'0a0a0a', '0e0e0e'
|
27
|
+
)
|
28
|
+
puts
|
29
|
+
puts "---"
|
30
|
+
pp colors = gradient.colors( 15 ) ## 15 steps (3x5)
|
31
|
+
puts "---"
|
32
|
+
pp colors.map { |color| Pixelart::Color.format( color ) }
|
33
|
+
|
34
|
+
|
35
|
+
|
36
|
+
####################
|
37
|
+
# https://uigradients.com/#Instagram
|
38
|
+
|
39
|
+
uigradients = {
|
40
|
+
'instagram' => ['833ab4',
|
41
|
+
'fd1d1d', 'fcb045'],
|
42
|
+
'flare' => ['f12711', 'f5af19'],
|
43
|
+
'terminal' => ['000000', '0f9b0f'],
|
44
|
+
'the_blue_lagoon' => ['43c6ac', '191654'],
|
45
|
+
'ibiza_sunset' => ['ee0979', 'ff6a00'],
|
46
|
+
'superman' => ['0099f7', 'f11712'],
|
47
|
+
'christmas' => ['2f7336', 'aa3a38'],
|
48
|
+
'dark_knight' => ['ba8b02', '181818'],
|
49
|
+
'ukraine' => ['004ff9', 'fff94c'],
|
50
|
+
}
|
51
|
+
|
52
|
+
|
53
|
+
|
54
|
+
uigradients.each do |name, stops|
|
55
|
+
puts
|
56
|
+
puts "==> #{name}:"
|
57
|
+
gradient = Pixelart::Gradient.new( *stops )
|
58
|
+
colors = gradient.colors( 256 )
|
59
|
+
pp colors.map { |color| Pixelart::Color.format( color ) }
|
60
|
+
end
|
61
|
+
|
62
|
+
puts "bye"
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pixelart-colors
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Gerald Bauer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-07-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rdoc
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.0'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '7'
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '4.0'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '7'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: hoe
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '3.23'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '3.23'
|
47
|
+
description: pixelart-colors - helpers to work with true colors as integers; 256 shades
|
48
|
+
for r/g/b (red, green and blue) totaling 16 million colors; incl. hsl, hsv color
|
49
|
+
space converters and more
|
50
|
+
email: wwwmake@googlegroups.com
|
51
|
+
executables: []
|
52
|
+
extensions: []
|
53
|
+
extra_rdoc_files:
|
54
|
+
- CHANGELOG.md
|
55
|
+
- Manifest.txt
|
56
|
+
- README.md
|
57
|
+
files:
|
58
|
+
- CHANGELOG.md
|
59
|
+
- Manifest.txt
|
60
|
+
- README.md
|
61
|
+
- Rakefile
|
62
|
+
- lib/pixelart/colors.rb
|
63
|
+
- lib/pixelart/colors/base.rb
|
64
|
+
- lib/pixelart/colors/color.rb
|
65
|
+
- lib/pixelart/colors/format.rb
|
66
|
+
- lib/pixelart/colors/gradient.rb
|
67
|
+
- lib/pixelart/colors/palette.rb
|
68
|
+
- lib/pixelart/colors/version.rb
|
69
|
+
- sandbox/test_colors.rb
|
70
|
+
- sandbox/test_gradient.rb
|
71
|
+
homepage: https://github.com/pixelartexchange/pixelart
|
72
|
+
licenses:
|
73
|
+
- Public Domain
|
74
|
+
metadata: {}
|
75
|
+
post_install_message:
|
76
|
+
rdoc_options:
|
77
|
+
- "--main"
|
78
|
+
- README.md
|
79
|
+
require_paths:
|
80
|
+
- lib
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '2.3'
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
requirements: []
|
92
|
+
rubygems_version: 3.3.7
|
93
|
+
signing_key:
|
94
|
+
specification_version: 4
|
95
|
+
summary: pixelart-colors - helpers to work with true colors as integers; 256 shades
|
96
|
+
for r/g/b (red, green and blue) totaling 16 million colors; incl. hsl, hsv color
|
97
|
+
space converters and more
|
98
|
+
test_files: []
|