pixelart 0.1.0 → 0.1.5
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/Manifest.txt +6 -0
- data/README.md +60 -15
- data/lib/pixelart.rb +6 -115
- data/lib/pixelart/base.rb +34 -0
- data/lib/pixelart/color.rb +132 -0
- data/lib/pixelart/gradient.rb +106 -0
- data/lib/pixelart/image.rb +221 -0
- data/lib/pixelart/misc.rb +37 -0
- data/lib/pixelart/palette.rb +72 -0
- data/lib/pixelart/version.rb +1 -1
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3022f23a6bfd66ef0e1c6f266fe2fd2ad24c802844be57737c2f202988e45292
|
|
4
|
+
data.tar.gz: b8d11d3128aa897ef91f21dc727f3f9f713a94343142bbeb0cce228dfdba0e6b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '086d0b53c8ef6a949025fcd2cda50f505deae181e53bc6cc6241beec8470bbc9f6869a03f189cbf56c6706635e83e16a799dda4e383823dd032a1bb0029717c2'
|
|
7
|
+
data.tar.gz: 3f577a57cd276d6a9eefdcf5374e5ea69e4f0f8f75cd69957098ec8c051258f70755c0883f8287bcf6e3768e35934bca4863d7ac41f55df7d1ae2395b4e12eed
|
data/Manifest.txt
CHANGED
data/README.md
CHANGED
|
@@ -58,21 +58,22 @@ Note: The color 0 (transparent) is auto-magically added / defined.
|
|
|
58
58
|
And let's mint a mooncat image:
|
|
59
59
|
|
|
60
60
|
``` ruby
|
|
61
|
-
img =
|
|
61
|
+
img = Image.parse( pixels, colors: colors )
|
|
62
62
|
img.save( './i/mooncat_white.png' )
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
And let's try a 3x zoom factor:
|
|
66
66
|
|
|
67
67
|
``` ruby
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
img3x = img.zoom( 3 )
|
|
69
|
+
img3x.save( './i/mooncat_white-3x.png' )
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
Voila!
|
|
73
73
|
|
|
74
|
-

|
|
75
|
-

|
|
74
|
+

|
|
75
|
+

|
|
76
|
+
|
|
76
77
|
|
|
77
78
|
|
|
78
79
|
Let's change the colors to use the genesis black color scheme:
|
|
@@ -90,17 +91,17 @@ colors = [
|
|
|
90
91
|
And let's start minting:
|
|
91
92
|
|
|
92
93
|
``` ruby
|
|
93
|
-
img =
|
|
94
|
+
img = Image.parse( pixels, colors: colors )
|
|
94
95
|
img.save( './i/mooncat_black.png' )
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
img3x = img.zoom( 3 )
|
|
98
|
+
img3x.save( './i/mooncat_black-3x.png' )
|
|
98
99
|
```
|
|
99
100
|
|
|
100
101
|
Voila! Black is the new White!
|
|
101
102
|
|
|
102
|
-

|
|
103
|
-

|
|
103
|
+

|
|
104
|
+

|
|
104
105
|
|
|
105
106
|
|
|
106
107
|
|
|
@@ -213,21 +214,65 @@ colors = {
|
|
|
213
214
|
And let's mint an imperial master image:
|
|
214
215
|
|
|
215
216
|
``` ruby
|
|
216
|
-
img =
|
|
217
|
+
img = Image.parse( pixels, colors: colors )
|
|
217
218
|
img.save( './i/vader.png' )
|
|
218
219
|
```
|
|
219
220
|
|
|
220
221
|
And let's try a 5x zoom factor:
|
|
221
222
|
|
|
222
223
|
``` ruby
|
|
223
|
-
|
|
224
|
-
|
|
224
|
+
img5x = img.zoom( 5 )
|
|
225
|
+
img5x.save( './i/vader5x.png' )
|
|
225
226
|
```
|
|
226
227
|
|
|
227
228
|
Voila!
|
|
228
229
|
|
|
229
|
-

|
|
230
|
-

|
|
230
|
+

|
|
231
|
+

|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
## Modular "Base" Version
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
Note: By default if you require pixelart
|
|
240
|
+
all classes inside the `Pixelart` module such as `Image`, `Color`, `Gradient`, `Palette8bit`, etc. get "top-leveled", that is,
|
|
241
|
+
included in the top level e.g.:
|
|
242
|
+
|
|
243
|
+
``` ruby
|
|
244
|
+
require 'pixelart/base'
|
|
245
|
+
include Pixelart
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
And now you can use all classes without
|
|
249
|
+
the `Pixelart::` module scope e.g.:
|
|
250
|
+
|
|
251
|
+
``` ruby
|
|
252
|
+
gradient = Gradient.new( '000000', 'ffffff' )
|
|
253
|
+
|
|
254
|
+
pp colors = gradient.colors( 256 )
|
|
255
|
+
puts '---'
|
|
256
|
+
pp colors.map { |color| Color.to_hex( color ) }
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
vs
|
|
260
|
+
|
|
261
|
+
``` ruby
|
|
262
|
+
gradient = Pixelart::Gradient.new( '000000', 'ffffff' )
|
|
263
|
+
|
|
264
|
+
pp colors = gradient.colors( 256 )
|
|
265
|
+
puts '---'
|
|
266
|
+
pp colors.map { |color| Pixelart::Color.to_hex( color ) }
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
For a "stricter" modular version require the "base" version
|
|
271
|
+
that always requires the `Pixelart::` module scope e.g.:
|
|
272
|
+
|
|
273
|
+
``` ruby
|
|
274
|
+
require 'pixelart/base'
|
|
275
|
+
```
|
|
231
276
|
|
|
232
277
|
|
|
233
278
|
|
data/lib/pixelart.rb
CHANGED
|
@@ -1,119 +1,10 @@
|
|
|
1
|
-
## 3rd party
|
|
2
|
-
require 'chunky_png'
|
|
3
1
|
|
|
4
|
-
##
|
|
5
|
-
require '
|
|
2
|
+
## our own code (without "top-level" shortcuts e.g. "modular version")
|
|
3
|
+
require 'pixelart/base' # aka "strict(er)" version
|
|
6
4
|
|
|
7
|
-
## our own code
|
|
8
|
-
require 'pixelart/version' # note: let version always go first
|
|
9
5
|
|
|
6
|
+
###
|
|
7
|
+
# add convenience top-level shortcuts / aliases
|
|
8
|
+
# make Image, Color, Palette8bit, etc top-level
|
|
9
|
+
include Pixelart
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
module Pixelart
|
|
13
|
-
class Image
|
|
14
|
-
|
|
15
|
-
def initialize( pixels, colors:, zoom: 1 )
|
|
16
|
-
@colors = parse_colors( colors )
|
|
17
|
-
@pixels = parse_pixels( pixels )
|
|
18
|
-
|
|
19
|
-
max_width = @pixels.reduce(1) {|max_width,row| row.size > max_width ? row.size : max_width }
|
|
20
|
-
max_height = @pixels.size
|
|
21
|
-
|
|
22
|
-
@img = ChunkyPNG::Image.new( max_width*zoom,
|
|
23
|
-
max_height*zoom,
|
|
24
|
-
ChunkyPNG::Color::TRANSPARENT )
|
|
25
|
-
|
|
26
|
-
@pixels.each_with_index do |row,y|
|
|
27
|
-
row.each_with_index do |color,x|
|
|
28
|
-
pixel = @colors[color]
|
|
29
|
-
zoom.times do |n|
|
|
30
|
-
zoom.times do |m|
|
|
31
|
-
@img[n+zoom*x,m+zoom*y] = pixel
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end # each row
|
|
35
|
-
end # each data
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def parse_pixels( pixels )
|
|
40
|
-
data = []
|
|
41
|
-
pixels.each_line do |line|
|
|
42
|
-
line = line.strip
|
|
43
|
-
if line.empty?
|
|
44
|
-
puts "!! WARN: skipping empty line in pixel art source"
|
|
45
|
-
next
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
## note: allow multiple spaces or tabs to separate pixel codes
|
|
49
|
-
## e.g. o o o o o o o o o o o o dg lg w w lg w lg lg dg dg w w lg dg o o o o o o o o o o o
|
|
50
|
-
## or
|
|
51
|
-
data << line.split( /[ \t]+/)
|
|
52
|
-
end
|
|
53
|
-
data
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
#####
|
|
58
|
-
# (image) delegates
|
|
59
|
-
## todo/check: add some more??
|
|
60
|
-
def save( path, constraints = {} )
|
|
61
|
-
@img.save( path, constraints )
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def width() @img.width; end
|
|
65
|
-
def height() @img.height; end
|
|
66
|
-
|
|
67
|
-
## return image ref - use a different name - why? why not?
|
|
68
|
-
def image() @img; end
|
|
69
|
-
|
|
70
|
-
def colors() @colors; end ## todo/check - return color map (hash table) or just color values (array) - why? why not?
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
######
|
|
75
|
-
# helpers
|
|
76
|
-
def parse_colors( colors )
|
|
77
|
-
if colors.is_a?( Array ) ## convenience shortcut
|
|
78
|
-
## note: always auto-add color 0 as pre-defined transparent - why? why not?
|
|
79
|
-
h = { '0' => ChunkyPNG::Color::TRANSPARENT }
|
|
80
|
-
colors.each_with_index do |color, i|
|
|
81
|
-
h[ (i+1).to_s ] = parse_color( color )
|
|
82
|
-
end
|
|
83
|
-
h
|
|
84
|
-
else ## assume hash table with color map
|
|
85
|
-
## convert into ChunkyPNG::Color
|
|
86
|
-
colors.map do |key,color|
|
|
87
|
-
## always convert key to string why? why not? use symbol?
|
|
88
|
-
[ key.to_s, parse_color( color ) ]
|
|
89
|
-
end.to_h
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def parse_color( color )
|
|
94
|
-
if color.is_a?( Integer ) ## e.g. Assumess ChunkyPNG::Color.rgb() or such
|
|
95
|
-
color ## pass through as is 1:1
|
|
96
|
-
elsif color.is_a?(String)
|
|
97
|
-
if color.downcase == 'transparent' ## special case for builtin colors
|
|
98
|
-
ChunkyPNG::Color::TRANSPARENT
|
|
99
|
-
else
|
|
100
|
-
## note: return an Integer !!! (not a Color class or such!!! )
|
|
101
|
-
ChunkyPNG::Color.from_hex( color )
|
|
102
|
-
end
|
|
103
|
-
else
|
|
104
|
-
raise ArgumentError, "unknown color format; cannot parse - expected rgb hex string e.g. d3d3d3"
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
end # class Image
|
|
110
|
-
end # module Pixelart
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
### add some convenience shortcuts
|
|
115
|
-
PixelArt = Pixelart
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
puts Pixelart.banner # say hello
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
## 3rd party
|
|
2
|
+
require 'chunky_png'
|
|
3
|
+
|
|
4
|
+
## stdlib
|
|
5
|
+
require 'pp'
|
|
6
|
+
require 'time'
|
|
7
|
+
require 'date'
|
|
8
|
+
require 'fileutils'
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## our own code
|
|
12
|
+
require 'pixelart/version' # note: let version always go first
|
|
13
|
+
require 'pixelart/color'
|
|
14
|
+
require 'pixelart/gradient'
|
|
15
|
+
require 'pixelart/palette'
|
|
16
|
+
require 'pixelart/image'
|
|
17
|
+
|
|
18
|
+
require 'pixelart/misc' ## misc helpers
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
##########
|
|
22
|
+
# add some spelling convenience variants
|
|
23
|
+
PixelArt = Pixelart
|
|
24
|
+
|
|
25
|
+
module Pixelart
|
|
26
|
+
Palette256 = Palette8Bit = Palette8bit
|
|
27
|
+
|
|
28
|
+
Palette256Image = Palette8BitImage = Palette8bitImage =
|
|
29
|
+
ImagePalette256 = ImagePalette8Bit = ImagePalette8bit
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
puts Pixelart.banner # say hello
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
module Pixelart
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Color
|
|
5
|
+
TRANSPARENT = 0 # rgba( 0, 0, 0, 0)
|
|
6
|
+
BLACK = 0xff # rgba( 0, 0, 0,255)
|
|
7
|
+
WHITE = 0xffffffff # rgba(255,255,255,255)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def self.parse( color )
|
|
12
|
+
if color.is_a?( Integer ) ## e.g. assumes ChunkyPNG::Color.rgb() 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
|
+
def self.from_hex( hex )
|
|
29
|
+
## Creates a color by converting it from a string in hex notation.
|
|
30
|
+
##
|
|
31
|
+
## It supports colors with (#rrggbbaa) or without (#rrggbb)
|
|
32
|
+
## alpha channel as well as the 3-digit short format (#rgb)
|
|
33
|
+
## for those without. Color strings may include
|
|
34
|
+
## the prefix "0x" or "#"".
|
|
35
|
+
ChunkyPNG::Color.from_hex( hex )
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.from_hsl( hue, saturation, lightness, alpha=255)
|
|
39
|
+
ChunkyPNG::Color.from_hsl( hue,
|
|
40
|
+
saturation,
|
|
41
|
+
lightness,
|
|
42
|
+
alpha )
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def self.to_hex( color, include_alpha: true )
|
|
47
|
+
ChunkyPNG::Color.to_hex( color, include_alpha )
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.to_hsl( color, include_alpha: true )
|
|
51
|
+
# Returns an array with the separate HSL components of a color.
|
|
52
|
+
ChunkyPNG::Color.to_hsl( color, include_alpha )
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.r( color ) ChunkyPNG::Color.r( color ); end
|
|
56
|
+
def self.g( color ) ChunkyPNG::Color.g( color ); end
|
|
57
|
+
def self.b( color ) ChunkyPNG::Color.b( color ); end
|
|
58
|
+
|
|
59
|
+
def self.rgb( r, g, b ) ChunkyPNG::Color.rgb( r, g, b); end
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
## known built-in color names
|
|
64
|
+
def self.build_names
|
|
65
|
+
names = {
|
|
66
|
+
'#00000000' => 'TRANSPARENT',
|
|
67
|
+
'#000000ff' => 'BLACK',
|
|
68
|
+
'#ffffffff' => 'WHITE',
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
## auto-add grayscale 1 to 254
|
|
72
|
+
(1..254).each do |n|
|
|
73
|
+
hex = "#" + ('%02x' % n)*3
|
|
74
|
+
hex << "ff" ## add alpha channel (255)
|
|
75
|
+
names[ hex ] = "8-BIT GRAYSCALE ##{n}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
names
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
NAMES = build_names
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def self.format( color )
|
|
86
|
+
rgb = [r(color),
|
|
87
|
+
g(color),
|
|
88
|
+
b(color)]
|
|
89
|
+
|
|
90
|
+
# rgb in hex (string format)
|
|
91
|
+
# note: do NOT include alpha channel for now - why? why not?
|
|
92
|
+
hex = "#" + rgb.map{|num| '%02x' % num }.join
|
|
93
|
+
|
|
94
|
+
hsl = to_hsl( color )
|
|
95
|
+
## get alpha channel (transparency) for hsla
|
|
96
|
+
alpha = hsl[3]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
buf = ''
|
|
100
|
+
buf << hex
|
|
101
|
+
buf << " / "
|
|
102
|
+
buf << "rgb("
|
|
103
|
+
buf << "%3d " % rgb[0]
|
|
104
|
+
buf << "%3d " % rgb[1]
|
|
105
|
+
buf << "%3d)" % rgb[2]
|
|
106
|
+
buf << " - "
|
|
107
|
+
buf << "hsl("
|
|
108
|
+
buf << "%3d° " % (hsl[0] % 360)
|
|
109
|
+
buf << "%3d%% " % (hsl[1]*100+0.5).to_i
|
|
110
|
+
buf << "%3d%%)" % (hsl[2]*100+0.5).to_i
|
|
111
|
+
|
|
112
|
+
if alpha != 255
|
|
113
|
+
buf << " - α(%3d%%)" % (alpha*100/255+0.5).to_i
|
|
114
|
+
else
|
|
115
|
+
buf << " " ## add empty for 255 (full opacity)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
## note: add alpha channel to hex
|
|
119
|
+
alpha_hex = '%02x' % alpha
|
|
120
|
+
name = NAMES[ hex+alpha_hex ]
|
|
121
|
+
buf << " - #{name}" if name
|
|
122
|
+
|
|
123
|
+
buf
|
|
124
|
+
end
|
|
125
|
+
class << self
|
|
126
|
+
alias_method :fmt, :format
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
end # class Color
|
|
130
|
+
end # module Pixelart
|
|
131
|
+
|
|
132
|
+
|
|
@@ -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,221 @@
|
|
|
1
|
+
module Pixelart
|
|
2
|
+
|
|
3
|
+
class Image
|
|
4
|
+
|
|
5
|
+
def self.read( path ) ## convenience helper
|
|
6
|
+
img_inner = ChunkyPNG::Image.from_file( path )
|
|
7
|
+
img = new( img_inner.width, img_inner.height, img_inner )
|
|
8
|
+
img
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def self.parse( pixels, colors: )
|
|
13
|
+
colors = parse_colors( colors )
|
|
14
|
+
pixels = parse_pixels( pixels )
|
|
15
|
+
|
|
16
|
+
width = pixels.reduce(1) {|width,row| row.size > width ? row.size : width }
|
|
17
|
+
height = pixels.size
|
|
18
|
+
|
|
19
|
+
img = new( width, height )
|
|
20
|
+
|
|
21
|
+
pixels.each_with_index do |row,y|
|
|
22
|
+
row.each_with_index do |color,x|
|
|
23
|
+
pixel = colors[color]
|
|
24
|
+
img[x,y] = pixel
|
|
25
|
+
end # each row
|
|
26
|
+
end # each data
|
|
27
|
+
|
|
28
|
+
img
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def initialize( width, height, initial=Color::TRANSPARENT )
|
|
34
|
+
|
|
35
|
+
if initial.is_a?( ChunkyPNG::Image )
|
|
36
|
+
@img = initial
|
|
37
|
+
else
|
|
38
|
+
## todo/check - initial - use parse_color here too e.g. allow "#fff" too etc.
|
|
39
|
+
@img = ChunkyPNG::Image.new( width, height, initial )
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def zoom( zoom=2 )
|
|
46
|
+
## create a new zoom factor x image (2x, 3x, etc.)
|
|
47
|
+
|
|
48
|
+
img = Image.new( @img.width*zoom,
|
|
49
|
+
@img.height*zoom )
|
|
50
|
+
|
|
51
|
+
@img.height.times do |y|
|
|
52
|
+
@img.width.times do |x|
|
|
53
|
+
pixel = @img[x,y]
|
|
54
|
+
zoom.times do |n|
|
|
55
|
+
zoom.times do |m|
|
|
56
|
+
img[n+zoom*x,m+zoom*y] = pixel
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end # each x
|
|
60
|
+
end # each y
|
|
61
|
+
|
|
62
|
+
img
|
|
63
|
+
end
|
|
64
|
+
alias_method :scale, :zoom
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
#######################
|
|
69
|
+
## filter / effects
|
|
70
|
+
|
|
71
|
+
def grayscale
|
|
72
|
+
img = @img.grayscale
|
|
73
|
+
Image.new( img.width, img.height, img )
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
## add replace_colors alias too? - why? why not?
|
|
77
|
+
def change_colors( color_map )
|
|
78
|
+
color_map = _parse_color_map( color_map )
|
|
79
|
+
|
|
80
|
+
img = @img.dup ## note: make a deep copy!!!
|
|
81
|
+
_change_colors!( img, color_map )
|
|
82
|
+
|
|
83
|
+
## wrap into Pixelart::Image - lets you use zoom() and such
|
|
84
|
+
Image.new( img.width, img.height, img )
|
|
85
|
+
end
|
|
86
|
+
alias_method :recolor, :change_colors
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
## predefined palette8bit color maps
|
|
91
|
+
## (grayscale to sepia/blue/false/etc.)
|
|
92
|
+
## - todo/check - keep "shortcut" convenience predefined map - why? why not?
|
|
93
|
+
PALETTE8BIT = {
|
|
94
|
+
sepia: Palette8bit::GRAYSCALE.zip( Palette8bit::SEPIA ).to_h,
|
|
95
|
+
blue: Palette8bit::GRAYSCALE.zip( Palette8bit::BLUE ).to_h,
|
|
96
|
+
false: Palette8bit::GRAYSCALE.zip( Palette8bit::FALSE ).to_h,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
def change_palette8bit( palette )
|
|
100
|
+
## step 0: mapping from grayscale to new 8bit palette (256 colors)
|
|
101
|
+
color_map = if palette.is_a?( String ) || palette.is_a?( Symbol )
|
|
102
|
+
PALETTE8BIT[ palette.to_sym ]
|
|
103
|
+
## todo/fix: check for missing/undefined palette not found - why? why not?
|
|
104
|
+
else
|
|
105
|
+
## make sure we have colors all in Integer not names, hex, etc.
|
|
106
|
+
palette = _parse_colors( palette )
|
|
107
|
+
Palette8bit::GRAYSCALE.zip( palette ).to_h
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
## step 1: convert to grayscale (256 colors)
|
|
111
|
+
img = @img.grayscale
|
|
112
|
+
_change_colors!( img, color_map )
|
|
113
|
+
|
|
114
|
+
## wrap into Pixelart::Image - lets you use zoom() and such
|
|
115
|
+
Image.new( img.width, img.height, img )
|
|
116
|
+
end
|
|
117
|
+
alias_method :change_palette256, :change_palette8bit
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
####
|
|
121
|
+
## private helpers
|
|
122
|
+
def _parse_colors( colors )
|
|
123
|
+
colors.map {|color| Color.parse( color ) }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def _parse_color_map( color_map )
|
|
127
|
+
color_map.map do |k,v|
|
|
128
|
+
[Color.parse(k), Color.parse(v)]
|
|
129
|
+
end.to_h
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def _change_colors!( img, color_map )
|
|
133
|
+
img.width.times do |x|
|
|
134
|
+
img.height.times do |y|
|
|
135
|
+
color = img[x,y]
|
|
136
|
+
new_color = color_map[color]
|
|
137
|
+
img[x,y] = new_color if new_color
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
#####
|
|
146
|
+
# (image) delegates
|
|
147
|
+
## todo/check: add some more??
|
|
148
|
+
def save( path, constraints = {} )
|
|
149
|
+
# step 1: make sure outdir exits
|
|
150
|
+
outdir = File.dirname( path )
|
|
151
|
+
FileUtils.mkdir_p( outdir ) unless Dir.exist?( outdir )
|
|
152
|
+
|
|
153
|
+
# step 2: save
|
|
154
|
+
@img.save( path, constraints )
|
|
155
|
+
end
|
|
156
|
+
alias_method :write, :save
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def compose!( other, x=0, y=0 )
|
|
160
|
+
@img.compose!( other.image, x, y ) ## note: "unwrap" inner image ref
|
|
161
|
+
end
|
|
162
|
+
alias_method :paste!, :compose!
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def width() @img.width; end
|
|
166
|
+
def height() @img.height; end
|
|
167
|
+
|
|
168
|
+
def []( x, y ) @img[x,y]; end
|
|
169
|
+
def []=( x, y, value ) @img[x,y]=value; end
|
|
170
|
+
|
|
171
|
+
def pixels() @img.pixels; end
|
|
172
|
+
|
|
173
|
+
## return image ref - use a different name - why? why not?
|
|
174
|
+
## change to to_image - why? why not?
|
|
175
|
+
def image() @img; end
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
######
|
|
181
|
+
# helpers
|
|
182
|
+
def self.parse_pixels( pixels )
|
|
183
|
+
data = []
|
|
184
|
+
pixels.each_line do |line|
|
|
185
|
+
line = line.strip
|
|
186
|
+
if line.empty?
|
|
187
|
+
puts "!! WARN: skipping empty line in pixel art source"
|
|
188
|
+
next
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
## note: allow multiple spaces or tabs to separate pixel codes
|
|
192
|
+
## e.g. o o o o o o o o o o o o dg lg w w lg w lg lg dg dg w w lg dg o o o o o o o o o o o
|
|
193
|
+
## or
|
|
194
|
+
data << line.split( /[ \t]+/)
|
|
195
|
+
end
|
|
196
|
+
data
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def self.parse_colors( colors )
|
|
201
|
+
if colors.is_a?( Array ) ## convenience shortcut
|
|
202
|
+
## note: always auto-add color 0 as pre-defined transparent - why? why not?
|
|
203
|
+
h = { '0' => Color::TRANSPARENT }
|
|
204
|
+
colors.each_with_index do |color, i|
|
|
205
|
+
h[ (i+1).to_s ] = Color.parse( color )
|
|
206
|
+
end
|
|
207
|
+
h
|
|
208
|
+
else ## assume hash table with color map
|
|
209
|
+
## convert into ChunkyPNG::Color
|
|
210
|
+
colors.map do |key,color|
|
|
211
|
+
## always convert key to string why? why not? use symbol?
|
|
212
|
+
[ key.to_s, Color.parse( color ) ]
|
|
213
|
+
end.to_h
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
end # class Image
|
|
220
|
+
end # module Pixelart
|
|
221
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Pixelart
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ImagePalette8bit < Image # or use Palette256 alias?
|
|
5
|
+
def initialize( colors, size: 1, spacing: nil )
|
|
6
|
+
## todo/check: change size arg to pixel or such? better name/less confusing - why? why not?
|
|
7
|
+
|
|
8
|
+
## todo/check: assert colors MUST have 256 colors!!!!
|
|
9
|
+
|
|
10
|
+
## use a "smart" default if no spacing set
|
|
11
|
+
## 0 for for (pixel) size == 1
|
|
12
|
+
## 1 for the rest
|
|
13
|
+
spacing = size == 1 ? 0 : 1 if spacing.nil?
|
|
14
|
+
|
|
15
|
+
img = ChunkyPNG::Image.new( 32*size+(32-1)*spacing,
|
|
16
|
+
8*size+(8-1)*spacing )
|
|
17
|
+
|
|
18
|
+
colors.each_with_index do |color,i|
|
|
19
|
+
y,x = i.divmod( 32 )
|
|
20
|
+
if size > 1
|
|
21
|
+
size.times do |n|
|
|
22
|
+
size.times do |m|
|
|
23
|
+
img[ x*size+n+spacing*x,
|
|
24
|
+
y*size+m+spacing*y] = color
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
else
|
|
28
|
+
img[x,y] = color
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
super( img.width, img.height, img )
|
|
33
|
+
end
|
|
34
|
+
end # class ImagePalette8bit
|
|
35
|
+
end # module Pixelart
|
|
36
|
+
|
|
37
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module Pixelart
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Palette8bit # or use Palette256 alias?
|
|
5
|
+
|
|
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
|
+
|
data/lib/pixelart/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pixelart
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gerald Bauer
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2021-04-
|
|
11
|
+
date: 2021-04-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: chunky_png
|
|
@@ -73,6 +73,12 @@ files:
|
|
|
73
73
|
- README.md
|
|
74
74
|
- Rakefile
|
|
75
75
|
- lib/pixelart.rb
|
|
76
|
+
- lib/pixelart/base.rb
|
|
77
|
+
- lib/pixelart/color.rb
|
|
78
|
+
- lib/pixelart/gradient.rb
|
|
79
|
+
- lib/pixelart/image.rb
|
|
80
|
+
- lib/pixelart/misc.rb
|
|
81
|
+
- lib/pixelart/palette.rb
|
|
76
82
|
- lib/pixelart/version.rb
|
|
77
83
|
homepage: https://github.com/cryptocopycats/mooncats
|
|
78
84
|
licenses:
|