pixelart 1.2.3 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,283 +1,283 @@
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
-
13
- CHARS = '.@xo^~%*+=:' ## todo/check: rename to default chars or such? why? why not?
14
-
15
- ## todo/check: support default chars encoding auto-of-the-box always
16
- ## or require user-defined chars to be passed in - why? why not?
17
- def self.parse( pixels, colors:, chars: CHARS )
18
- has_keys = colors.is_a?(Hash) ## check if passed-in user-defined keys (via hash table)?
19
-
20
- colors = parse_colors( colors )
21
- pixels = parse_pixels( pixels )
22
-
23
- width = pixels.reduce(1) {|width,row| row.size > width ? row.size : width }
24
- height = pixels.size
25
-
26
- img = new( width, height )
27
-
28
- pixels.each_with_index do |row,y|
29
- row.each_with_index do |color,x|
30
- pixel = if has_keys ## if passed-in user-defined keys check only the user-defined keys
31
- colors[color]
32
- else
33
- ## try map ascii art char (.@xo etc.) to color index (0,1,2)
34
- ## if no match found - fallback on assuming draw by number (0 1 2 etc.) encoding
35
- pos = chars.index( color )
36
- if pos
37
- colors[ pos.to_s ]
38
- else ## assume nil (not found)
39
- colors[ color ]
40
- end
41
- end
42
-
43
- img[x,y] = pixel
44
- end # each row
45
- end # each data
46
-
47
- img
48
- end
49
-
50
-
51
-
52
- def initialize( width, height, initial=Color::TRANSPARENT )
53
- ### todo/fix:
54
- ## change params to *args only - why? why not?
55
- ## make width/height optional if image passed in?
56
-
57
- if initial.is_a?( ChunkyPNG::Image )
58
- @img = initial
59
- else
60
- ## todo/check - initial - use parse_color here too e.g. allow "#fff" too etc.
61
- @img = ChunkyPNG::Image.new( width, height, initial )
62
- end
63
- end
64
-
65
-
66
-
67
- def zoom( zoom=2, spacing: 0 )
68
- ## create a new zoom factor x image (2x, 3x, etc.)
69
-
70
- width = @img.width*zoom+(@img.width-1)*spacing
71
- height = @img.height*zoom+(@img.height-1)*spacing
72
-
73
- img = Image.new( width, height )
74
-
75
- @img.width.times do |x|
76
- @img.height.times do |y|
77
- pixel = @img[x,y]
78
- zoom.times do |n|
79
- zoom.times do |m|
80
- img[n+zoom*x+spacing*x,
81
- m+zoom*y+spacing*y] = pixel
82
- end
83
- end
84
- end # each x
85
- end # each y
86
-
87
- img
88
- end
89
- alias_method :scale, :zoom
90
-
91
-
92
- def crop( x, y, crop_width, crop_height )
93
- Image.new( nil, nil,
94
- image.crop( x,y, crop_width, crop_height ) )
95
- end
96
-
97
-
98
-
99
- #######################
100
- ## filter / effects
101
-
102
- def grayscale
103
- img = @img.grayscale
104
- Image.new( img.width, img.height, img )
105
- end
106
- alias_method :greyscale, :grayscale
107
-
108
-
109
-
110
- def flip
111
- img = @img.flip
112
- Image.new( img.width, img.height, img )
113
- end
114
- alias_method :flip_horizontally, :flip
115
-
116
- def mirror
117
- img = @img.mirror
118
- Image.new( img.width, img.height, img )
119
- end
120
- alias_method :flip_vertically, :mirror
121
- alias_method :flop, :mirror
122
-
123
-
124
- def rotate_counter_clockwise # 90 degrees
125
- img = @img.rotate_counter_clockwise
126
- Image.new( img.width, img.height, img )
127
- end
128
- alias_method :rotate_left, :rotate_counter_clockwise
129
-
130
- def rotate_clockwise # 90 degrees
131
- img = @img.rotate_clockwise
132
- Image.new( img.width, img.height, img )
133
- end
134
- alias_method :rotate_right, :rotate_clockwise
135
-
136
-
137
-
138
- ## add replace_colors alias too? - why? why not?
139
- def change_colors( color_map )
140
- color_map = _parse_color_map( color_map )
141
-
142
- img = @img.dup ## note: make a deep copy!!!
143
- _change_colors!( img, color_map )
144
-
145
- ## wrap into Pixelart::Image - lets you use zoom() and such
146
- Image.new( img.width, img.height, img )
147
- end
148
- alias_method :recolor, :change_colors
149
-
150
-
151
-
152
- ## predefined palette8bit color maps
153
- ## (grayscale to sepia/blue/false/etc.)
154
- ## - todo/check - keep "shortcut" convenience predefined map - why? why not?
155
- PALETTE8BIT = {
156
- sepia: Palette8bit::GRAYSCALE.zip( Palette8bit::SEPIA ).to_h,
157
- blue: Palette8bit::GRAYSCALE.zip( Palette8bit::BLUE ).to_h,
158
- false: Palette8bit::GRAYSCALE.zip( Palette8bit::FALSE ).to_h,
159
- }
160
-
161
- def change_palette8bit( palette )
162
- ## step 0: mapping from grayscale to new 8bit palette (256 colors)
163
- color_map = if palette.is_a?( String ) || palette.is_a?( Symbol )
164
- PALETTE8BIT[ palette.to_sym ]
165
- ## todo/fix: check for missing/undefined palette not found - why? why not?
166
- else
167
- ## make sure we have colors all in Integer not names, hex, etc.
168
- palette = _parse_colors( palette )
169
- Palette8bit::GRAYSCALE.zip( palette ).to_h
170
- end
171
-
172
- ## step 1: convert to grayscale (256 colors)
173
- img = @img.grayscale
174
- _change_colors!( img, color_map )
175
-
176
- ## wrap into Pixelart::Image - lets you use zoom() and such
177
- Image.new( img.width, img.height, img )
178
- end
179
- alias_method :change_palette256, :change_palette8bit
180
-
181
-
182
- ####
183
- ## private helpers
184
- def _parse_colors( colors )
185
- colors.map {|color| Color.parse( color ) }
186
- end
187
-
188
- def _parse_color_map( color_map )
189
- color_map.map do |k,v|
190
- [Color.parse(k), Color.parse(v)]
191
- end.to_h
192
- end
193
-
194
- def _change_colors!( img, color_map )
195
- img.width.times do |x|
196
- img.height.times do |y|
197
- color = img[x,y]
198
- new_color = color_map[color]
199
- img[x,y] = new_color if new_color
200
- end
201
- end
202
- end
203
-
204
-
205
-
206
-
207
- #####
208
- # (image) delegates
209
- ## todo/check: add some more??
210
- def save( path, constraints = {} )
211
- # step 1: make sure outdir exits
212
- outdir = File.dirname( path )
213
- FileUtils.mkdir_p( outdir ) unless Dir.exist?( outdir )
214
-
215
- # step 2: save
216
- @img.save( path, constraints )
217
- end
218
- alias_method :write, :save
219
-
220
-
221
- def compose!( other, x=0, y=0 )
222
- @img.compose!( other.image, x, y ) ## note: "unwrap" inner image ref
223
- end
224
- alias_method :paste!, :compose!
225
-
226
-
227
- def width() @img.width; end
228
- def height() @img.height; end
229
-
230
- def []( x, y ) @img[x,y]; end
231
- def []=( x, y, value ) @img[x,y]=value; end
232
-
233
- def pixels() @img.pixels; end
234
-
235
- ### todo/check: add colors() e.g. @img.pixels.uniq - why? why not?
236
-
237
-
238
- ## return image ref - use a different name - why? why not?
239
- ## change to to_image - why? why not?
240
- def image() @img; end
241
-
242
-
243
-
244
-
245
- ######
246
- # helpers
247
- def self.parse_pixels( pixels )
248
- data = []
249
- pixels.each_line do |line|
250
- line = line.strip
251
- next if line.start_with?( '#' ) || line.empty? ## note: allow comments and empty lines
252
-
253
- ## note: allow multiple spaces or tabs to separate pixel codes
254
- ## 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
255
- ## or
256
- data << line.split( /[ \t]+/)
257
- end
258
- data
259
- end
260
-
261
-
262
-
263
- def self.parse_colors( colors )
264
- if colors.is_a?( Array ) ## convenience shortcut
265
- ## note: always auto-add color 0 as pre-defined transparent - why? why not?
266
- h = { '0' => Color::TRANSPARENT }
267
- colors.each_with_index do |color, i|
268
- h[ (i+1).to_s ] = Color.parse( color )
269
- end
270
- h
271
- else ## assume hash table with color map
272
- ## convert into ChunkyPNG::Color
273
- colors.map do |key,color|
274
- ## always convert key to string why? why not? use symbol?
275
- [ key.to_s, Color.parse( color ) ]
276
- end.to_h
277
- end
278
- end
279
-
280
-
281
- end # class Image
282
- end # module Pixelart
283
-
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
+
13
+ CHARS = '.@xo^~%*+=:' ## todo/check: rename to default chars or such? why? why not?
14
+
15
+ ## todo/check: support default chars encoding auto-of-the-box always
16
+ ## or require user-defined chars to be passed in - why? why not?
17
+ def self.parse( pixels, colors:, chars: CHARS )
18
+ has_keys = colors.is_a?(Hash) ## check if passed-in user-defined keys (via hash table)?
19
+
20
+ colors = parse_colors( colors )
21
+ pixels = parse_pixels( pixels )
22
+
23
+ width = pixels.reduce(1) {|width,row| row.size > width ? row.size : width }
24
+ height = pixels.size
25
+
26
+ img = new( width, height )
27
+
28
+ pixels.each_with_index do |row,y|
29
+ row.each_with_index do |color,x|
30
+ pixel = if has_keys ## if passed-in user-defined keys check only the user-defined keys
31
+ colors[color]
32
+ else
33
+ ## try map ascii art char (.@xo etc.) to color index (0,1,2)
34
+ ## if no match found - fallback on assuming draw by number (0 1 2 etc.) encoding
35
+ pos = chars.index( color )
36
+ if pos
37
+ colors[ pos.to_s ]
38
+ else ## assume nil (not found)
39
+ colors[ color ]
40
+ end
41
+ end
42
+
43
+ img[x,y] = pixel
44
+ end # each row
45
+ end # each data
46
+
47
+ img
48
+ end
49
+
50
+
51
+
52
+ def initialize( width, height, initial=Color::TRANSPARENT )
53
+ ### todo/fix:
54
+ ## change params to *args only - why? why not?
55
+ ## make width/height optional if image passed in?
56
+
57
+ if initial.is_a?( ChunkyPNG::Image )
58
+ @img = initial
59
+ else
60
+ ## todo/check - initial - use parse_color here too e.g. allow "#fff" too etc.
61
+ @img = ChunkyPNG::Image.new( width, height, initial )
62
+ end
63
+ end
64
+
65
+
66
+
67
+ def zoom( zoom=2, spacing: 0 )
68
+ ## create a new zoom factor x image (2x, 3x, etc.)
69
+
70
+ width = @img.width*zoom+(@img.width-1)*spacing
71
+ height = @img.height*zoom+(@img.height-1)*spacing
72
+
73
+ img = Image.new( width, height )
74
+
75
+ @img.width.times do |x|
76
+ @img.height.times do |y|
77
+ pixel = @img[x,y]
78
+ zoom.times do |n|
79
+ zoom.times do |m|
80
+ img[n+zoom*x+spacing*x,
81
+ m+zoom*y+spacing*y] = pixel
82
+ end
83
+ end
84
+ end # each x
85
+ end # each y
86
+
87
+ img
88
+ end
89
+ alias_method :scale, :zoom
90
+
91
+
92
+ def crop( x, y, crop_width, crop_height )
93
+ Image.new( nil, nil,
94
+ image.crop( x,y, crop_width, crop_height ) )
95
+ end
96
+
97
+
98
+
99
+ #######################
100
+ ## filter / effects
101
+
102
+ def grayscale
103
+ img = @img.grayscale
104
+ Image.new( img.width, img.height, img )
105
+ end
106
+ alias_method :greyscale, :grayscale
107
+
108
+
109
+
110
+ def flip
111
+ img = @img.flip
112
+ Image.new( img.width, img.height, img )
113
+ end
114
+ alias_method :flip_horizontally, :flip
115
+
116
+ def mirror
117
+ img = @img.mirror
118
+ Image.new( img.width, img.height, img )
119
+ end
120
+ alias_method :flip_vertically, :mirror
121
+ alias_method :flop, :mirror
122
+
123
+
124
+ def rotate_counter_clockwise # 90 degrees
125
+ img = @img.rotate_counter_clockwise
126
+ Image.new( img.width, img.height, img )
127
+ end
128
+ alias_method :rotate_left, :rotate_counter_clockwise
129
+
130
+ def rotate_clockwise # 90 degrees
131
+ img = @img.rotate_clockwise
132
+ Image.new( img.width, img.height, img )
133
+ end
134
+ alias_method :rotate_right, :rotate_clockwise
135
+
136
+
137
+
138
+ ## add replace_colors alias too? - why? why not?
139
+ def change_colors( color_map )
140
+ color_map = _parse_color_map( color_map )
141
+
142
+ img = @img.dup ## note: make a deep copy!!!
143
+ _change_colors!( img, color_map )
144
+
145
+ ## wrap into Pixelart::Image - lets you use zoom() and such
146
+ Image.new( img.width, img.height, img )
147
+ end
148
+ alias_method :recolor, :change_colors
149
+
150
+
151
+
152
+ ## predefined palette8bit color maps
153
+ ## (grayscale to sepia/blue/false/etc.)
154
+ ## - todo/check - keep "shortcut" convenience predefined map - why? why not?
155
+ PALETTE8BIT = {
156
+ sepia: Palette8bit::GRAYSCALE.zip( Palette8bit::SEPIA ).to_h,
157
+ blue: Palette8bit::GRAYSCALE.zip( Palette8bit::BLUE ).to_h,
158
+ false: Palette8bit::GRAYSCALE.zip( Palette8bit::FALSE ).to_h,
159
+ }
160
+
161
+ def change_palette8bit( palette )
162
+ ## step 0: mapping from grayscale to new 8bit palette (256 colors)
163
+ color_map = if palette.is_a?( String ) || palette.is_a?( Symbol )
164
+ PALETTE8BIT[ palette.to_sym ]
165
+ ## todo/fix: check for missing/undefined palette not found - why? why not?
166
+ else
167
+ ## make sure we have colors all in Integer not names, hex, etc.
168
+ palette = _parse_colors( palette )
169
+ Palette8bit::GRAYSCALE.zip( palette ).to_h
170
+ end
171
+
172
+ ## step 1: convert to grayscale (256 colors)
173
+ img = @img.grayscale
174
+ _change_colors!( img, color_map )
175
+
176
+ ## wrap into Pixelart::Image - lets you use zoom() and such
177
+ Image.new( img.width, img.height, img )
178
+ end
179
+ alias_method :change_palette256, :change_palette8bit
180
+
181
+
182
+ ####
183
+ ## private helpers
184
+ def _parse_colors( colors )
185
+ colors.map {|color| Color.parse( color ) }
186
+ end
187
+
188
+ def _parse_color_map( color_map )
189
+ color_map.map do |k,v|
190
+ [Color.parse(k), Color.parse(v)]
191
+ end.to_h
192
+ end
193
+
194
+ def _change_colors!( img, color_map )
195
+ img.width.times do |x|
196
+ img.height.times do |y|
197
+ color = img[x,y]
198
+ new_color = color_map[color]
199
+ img[x,y] = new_color if new_color
200
+ end
201
+ end
202
+ end
203
+
204
+
205
+
206
+
207
+ #####
208
+ # (image) delegates
209
+ ## todo/check: add some more??
210
+ def save( path, constraints = {} )
211
+ # step 1: make sure outdir exits
212
+ outdir = File.dirname( path )
213
+ FileUtils.mkdir_p( outdir ) unless Dir.exist?( outdir )
214
+
215
+ # step 2: save
216
+ @img.save( path, constraints )
217
+ end
218
+ alias_method :write, :save
219
+
220
+
221
+ def compose!( other, x=0, y=0 )
222
+ @img.compose!( other.image, x, y ) ## note: "unwrap" inner image ref
223
+ end
224
+ alias_method :paste!, :compose!
225
+
226
+
227
+ def width() @img.width; end
228
+ def height() @img.height; end
229
+
230
+ def []( x, y ) @img[x,y]; end
231
+ def []=( x, y, value ) @img[x,y]=value; end
232
+
233
+ def pixels() @img.pixels; end
234
+
235
+ ### todo/check: add colors() e.g. @img.pixels.uniq - why? why not?
236
+
237
+
238
+ ## return image ref - use a different name - why? why not?
239
+ ## change to to_image - why? why not?
240
+ def image() @img; end
241
+
242
+
243
+
244
+
245
+ ######
246
+ # helpers
247
+ def self.parse_pixels( pixels )
248
+ data = []
249
+ pixels.each_line do |line|
250
+ line = line.strip
251
+ next if line.start_with?( '#' ) || line.empty? ## note: allow comments and empty lines
252
+
253
+ ## note: allow multiple spaces or tabs to separate pixel codes
254
+ ## 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
255
+ ## or
256
+ data << line.split( /[ \t]+/)
257
+ end
258
+ data
259
+ end
260
+
261
+
262
+
263
+ def self.parse_colors( colors )
264
+ if colors.is_a?( Array ) ## convenience shortcut
265
+ ## note: always auto-add color 0 as pre-defined transparent - why? why not?
266
+ h = { '0' => Color::TRANSPARENT }
267
+ colors.each_with_index do |color, i|
268
+ h[ (i+1).to_s ] = Color.parse( color )
269
+ end
270
+ h
271
+ else ## assume hash table with color map
272
+ ## convert into ChunkyPNG::Color
273
+ colors.map do |key,color|
274
+ ## always convert key to string why? why not? use symbol?
275
+ [ key.to_s, Color.parse( color ) ]
276
+ end.to_h
277
+ end
278
+ end
279
+
280
+
281
+ end # class Image
282
+ end # module Pixelart
283
+
data/lib/pixelart/led.rb CHANGED
@@ -1,37 +1,37 @@
1
- module Pixelart
2
-
3
-
4
- class Image
5
- def led( led=8, spacing: 2, round_corner: false )
6
-
7
- width = @img.width*led + (@img.width-1)*spacing
8
- height = @img.height*led + (@img.height-1)*spacing
9
-
10
- puts " #{width}x#{height}"
11
-
12
- img = Image.new( width, height, Color::BLACK )
13
-
14
- @img.width.times do |x|
15
- @img.height.times do |y|
16
- pixel = @img[x,y]
17
- pixel = Color::BLACK if pixel == Color::TRANSPARENT
18
- led.times do |n|
19
- led.times do |m|
20
- ## round a little - drop all four corners for now
21
- next if round_corner &&
22
- [[0,0],[0,1],[1,0],[1,1],[0,2],[2,0],
23
- [0,led-1],[0,led-2],[1,led-1],[1,led-2],[0,led-3],[2,led-1],
24
- [led-1,0],[led-1,1],[led-2,0],[led-2,1],[led-1,2],[led-3,0],
25
- [led-1,led-1],[led-1,led-2],[led-2,led-1],[led-2,led-2],[led-1,led-3],[led-3,led-1],
26
- ].include?( [n,m] )
27
- img[x*led+n + spacing*x,
28
- y*led+m + spacing*y] = pixel
29
- end
30
- end
31
- end
32
- end
33
- img
34
- end
35
- end # class Image
36
- end # module Pixelart
37
-
1
+ module Pixelart
2
+
3
+
4
+ class Image
5
+ def led( led=8, spacing: 2, round_corner: false )
6
+
7
+ width = @img.width*led + (@img.width-1)*spacing
8
+ height = @img.height*led + (@img.height-1)*spacing
9
+
10
+ puts " #{width}x#{height}"
11
+
12
+ img = Image.new( width, height, Color::BLACK )
13
+
14
+ @img.width.times do |x|
15
+ @img.height.times do |y|
16
+ pixel = @img[x,y]
17
+ pixel = Color::BLACK if pixel == Color::TRANSPARENT
18
+ led.times do |n|
19
+ led.times do |m|
20
+ ## round a little - drop all four corners for now
21
+ next if round_corner &&
22
+ [[0,0],[0,1],[1,0],[1,1],[0,2],[2,0],
23
+ [0,led-1],[0,led-2],[1,led-1],[1,led-2],[0,led-3],[2,led-1],
24
+ [led-1,0],[led-1,1],[led-2,0],[led-2,1],[led-1,2],[led-3,0],
25
+ [led-1,led-1],[led-1,led-2],[led-2,led-1],[led-2,led-2],[led-1,led-3],[led-3,led-1],
26
+ ].include?( [n,m] )
27
+ img[x*led+n + spacing*x,
28
+ y*led+m + spacing*y] = pixel
29
+ end
30
+ end
31
+ end
32
+ end
33
+ img
34
+ end
35
+ end # class Image
36
+ end # module Pixelart
37
+