pixelart 0.1.1 → 0.1.6
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 +55 -11
- data/Rakefile +1 -1
- data/lib/pixelart.rb +6 -176
- 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 +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: df39f58ffbca991ae1fbbdc2f4a3b83545074f7872dd96bf9b23ac5be7259a0d
|
4
|
+
data.tar.gz: 865908e9a481ef8bd74c98572dbe6c17fd2c7dde2bd5f0f8613247716be4065a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 65305e097f184eb6e62e6d0b392f2a9e42c2a705abbb198752fdb905681bc35ac9fb263efd663bb77d5ab3cef440127e55eeee2eb33ba841fde35338dff50d63
|
7
|
+
data.tar.gz: 94df6b1fced1c835ae8132922350b9d50d2fe21c29c020cc2b4595246af2d42fc0e0346bcfd6aacbc1ae45e153d58cfb5592ee90ab9c9da01b4ce07313d2b23e
|
data/Manifest.txt
CHANGED
data/README.md
CHANGED
@@ -3,8 +3,8 @@
|
|
3
3
|
pixelart - mint your own pixel art images off chain using any design (in ascii text) in any colors; incl. 2x/4x/8x zoom for bigger sizes
|
4
4
|
|
5
5
|
|
6
|
-
* home :: [github.com/
|
7
|
-
* bugs :: [github.com/
|
6
|
+
* home :: [github.com/rubycoco/pixel](https://github.com/rubycoco/pixel)
|
7
|
+
* bugs :: [github.com/rubycoco/pixel/issues](https://github.com/rubycoco/pixel/issues)
|
8
8
|
* gem :: [rubygems.org/gems/pixelart](https://rubygems.org/gems/pixelart)
|
9
9
|
* rdoc :: [rubydoc.info/gems/pixelart](http://rubydoc.info/gems/pixelart)
|
10
10
|
|
@@ -58,7 +58,7 @@ 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
|
|
@@ -71,8 +71,8 @@ img3x.save( './i/mooncat_white-3x.png' )
|
|
71
71
|
|
72
72
|
Voila!
|
73
73
|
|
74
|
-

|
75
|
+

|
76
76
|
|
77
77
|
|
78
78
|
|
@@ -91,7 +91,7 @@ colors = [
|
|
91
91
|
And let's start minting:
|
92
92
|
|
93
93
|
``` ruby
|
94
|
-
img =
|
94
|
+
img = Image.parse( pixels, colors: colors )
|
95
95
|
img.save( './i/mooncat_black.png' )
|
96
96
|
|
97
97
|
img3x = img.zoom( 3 )
|
@@ -100,8 +100,8 @@ img3x.save( './i/mooncat_black-3x.png' )
|
|
100
100
|
|
101
101
|
Voila! Black is the new White!
|
102
102
|
|
103
|
-

|
104
|
+

|
105
105
|
|
106
106
|
|
107
107
|
|
@@ -214,7 +214,7 @@ colors = {
|
|
214
214
|
And let's mint an imperial master image:
|
215
215
|
|
216
216
|
``` ruby
|
217
|
-
img =
|
217
|
+
img = Image.parse( pixels, colors: colors )
|
218
218
|
img.save( './i/vader.png' )
|
219
219
|
```
|
220
220
|
|
@@ -227,8 +227,52 @@ img5x.save( './i/vader5x.png' )
|
|
227
227
|
|
228
228
|
Voila!
|
229
229
|
|
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
|
+
```
|
232
276
|
|
233
277
|
|
234
278
|
|
data/Rakefile
CHANGED
@@ -8,7 +8,7 @@ Hoe.spec 'pixelart' do
|
|
8
8
|
self.summary = "pixelart - mint your own pixel art images off chain using any design (in ascii text) in any colors; incl. 2x/4x/8x zoom for bigger sizes"
|
9
9
|
self.description = summary
|
10
10
|
|
11
|
-
self.urls = { home: 'https://github.com/
|
11
|
+
self.urls = { home: 'https://github.com/rubycoco/pixel' }
|
12
12
|
|
13
13
|
self.author = 'Gerald Bauer'
|
14
14
|
self.email = 'wwwmake@googlegroups.com'
|
data/lib/pixelart.rb
CHANGED
@@ -1,180 +1,10 @@
|
|
1
|
-
## 3rd party
|
2
|
-
require 'chunky_png'
|
3
1
|
|
4
|
-
##
|
5
|
-
require '
|
6
|
-
require 'time'
|
7
|
-
require 'date'
|
8
|
-
require 'fileutils'
|
2
|
+
## our own code (without "top-level" shortcuts e.g. "modular version")
|
3
|
+
require 'pixelart/base' # aka "strict(er)" version
|
9
4
|
|
10
5
|
|
11
|
-
|
12
|
-
|
6
|
+
###
|
7
|
+
# add convenience top-level shortcuts / aliases
|
8
|
+
# make Image, Color, Palette8bit, etc top-level
|
9
|
+
include Pixelart
|
13
10
|
|
14
|
-
|
15
|
-
|
16
|
-
module Pixelart
|
17
|
-
class Image
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
def self.read( path ) ## convenience helper
|
22
|
-
img_inner = ChunkyPNG::Image.from_file( path )
|
23
|
-
img = new( img_inner.width, img_inner.height, img_inner )
|
24
|
-
img
|
25
|
-
end
|
26
|
-
|
27
|
-
|
28
|
-
def self.parse( pixels, colors: )
|
29
|
-
colors = parse_colors( colors )
|
30
|
-
pixels = parse_pixels( pixels )
|
31
|
-
|
32
|
-
width = pixels.reduce(1) {|width,row| row.size > width ? row.size : width }
|
33
|
-
height = pixels.size
|
34
|
-
|
35
|
-
img = new( width, height )
|
36
|
-
|
37
|
-
pixels.each_with_index do |row,y|
|
38
|
-
row.each_with_index do |color,x|
|
39
|
-
pixel = colors[color]
|
40
|
-
img[x,y] = pixel
|
41
|
-
end # each row
|
42
|
-
end # each data
|
43
|
-
|
44
|
-
img
|
45
|
-
end
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
def initialize( width, height, initial=ChunkyPNG::Color::TRANSPARENT )
|
50
|
-
|
51
|
-
if initial.is_a?( ChunkyPNG::Image )
|
52
|
-
@img = initial
|
53
|
-
else
|
54
|
-
## todo/check - initial - use parse_color here too e.g. allow "#fff" too etc.
|
55
|
-
@img = ChunkyPNG::Image.new( width, height, initial )
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
def zoom( zoom=2 )
|
62
|
-
## create a new zoom factor x image (2x, 3x, etc.)
|
63
|
-
|
64
|
-
img = Image.new( @img.width*zoom,
|
65
|
-
@img.height*zoom )
|
66
|
-
|
67
|
-
@img.height.times do |y|
|
68
|
-
@img.width.times do |x|
|
69
|
-
pixel = @img[x,y]
|
70
|
-
zoom.times do |n|
|
71
|
-
zoom.times do |m|
|
72
|
-
img[n+zoom*x,m+zoom*y] = pixel
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end # each x
|
76
|
-
end # each y
|
77
|
-
|
78
|
-
img
|
79
|
-
end
|
80
|
-
alias_method :scale, :zoom
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
#####
|
86
|
-
# (image) delegates
|
87
|
-
## todo/check: add some more??
|
88
|
-
def save( path, constraints = {} )
|
89
|
-
# step 1: make sure outdir exits
|
90
|
-
outdir = File.dirname( path )
|
91
|
-
FileUtils.mkdir_p( outdir ) unless Dir.exist?( outdir )
|
92
|
-
|
93
|
-
# step 2: save
|
94
|
-
@img.save( path, constraints )
|
95
|
-
end
|
96
|
-
alias_method :write, :save
|
97
|
-
|
98
|
-
|
99
|
-
def compose!( other, x=0, y=0 )
|
100
|
-
@img.compose!( other.image, x, y ) ## note: "unwrap" inner image ref
|
101
|
-
end
|
102
|
-
alias_method :paste!, :compose!
|
103
|
-
|
104
|
-
|
105
|
-
def width() @img.width; end
|
106
|
-
def height() @img.height; end
|
107
|
-
|
108
|
-
def []( x, y ) @img[x,y]; end
|
109
|
-
def []=( x, y, value ) @img[x,y]=value; end
|
110
|
-
|
111
|
-
## return image ref - use a different name - why? why not?
|
112
|
-
def image() @img; end
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
######
|
118
|
-
# helpers
|
119
|
-
def self.parse_pixels( pixels )
|
120
|
-
data = []
|
121
|
-
pixels.each_line do |line|
|
122
|
-
line = line.strip
|
123
|
-
if line.empty?
|
124
|
-
puts "!! WARN: skipping empty line in pixel art source"
|
125
|
-
next
|
126
|
-
end
|
127
|
-
|
128
|
-
## note: allow multiple spaces or tabs to separate pixel codes
|
129
|
-
## 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
|
130
|
-
## or
|
131
|
-
data << line.split( /[ \t]+/)
|
132
|
-
end
|
133
|
-
data
|
134
|
-
end
|
135
|
-
|
136
|
-
|
137
|
-
def self.parse_colors( colors )
|
138
|
-
if colors.is_a?( Array ) ## convenience shortcut
|
139
|
-
## note: always auto-add color 0 as pre-defined transparent - why? why not?
|
140
|
-
h = { '0' => ChunkyPNG::Color::TRANSPARENT }
|
141
|
-
colors.each_with_index do |color, i|
|
142
|
-
h[ (i+1).to_s ] = parse_color( color )
|
143
|
-
end
|
144
|
-
h
|
145
|
-
else ## assume hash table with color map
|
146
|
-
## convert into ChunkyPNG::Color
|
147
|
-
colors.map do |key,color|
|
148
|
-
## always convert key to string why? why not? use symbol?
|
149
|
-
[ key.to_s, parse_color( color ) ]
|
150
|
-
end.to_h
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
def self.parse_color( color )
|
155
|
-
if color.is_a?( Integer ) ## e.g. Assumess ChunkyPNG::Color.rgb() or such
|
156
|
-
color ## pass through as is 1:1
|
157
|
-
elsif color.is_a?(String)
|
158
|
-
if color.downcase == 'transparent' ## special case for builtin colors
|
159
|
-
ChunkyPNG::Color::TRANSPARENT
|
160
|
-
else
|
161
|
-
## note: return an Integer !!! (not a Color class or such!!! )
|
162
|
-
ChunkyPNG::Color.from_hex( color )
|
163
|
-
end
|
164
|
-
else
|
165
|
-
raise ArgumentError, "unknown color format; cannot parse - expected rgb hex string e.g. d3d3d3"
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
|
170
|
-
end # class Image
|
171
|
-
end # module Pixelart
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
### add some convenience shortcuts
|
176
|
-
PixelArt = Pixelart
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
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.6
|
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-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: chunky_png
|
@@ -73,8 +73,14 @@ 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
|
-
homepage: https://github.com/
|
83
|
+
homepage: https://github.com/rubycoco/pixel
|
78
84
|
licenses:
|
79
85
|
- Public Domain
|
80
86
|
metadata: {}
|