pixelart 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,266 +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
-
93
- #######################
94
- ## filter / effects
95
-
96
- def grayscale
97
- img = @img.grayscale
98
- Image.new( img.width, img.height, img )
99
- end
100
- alias_method :greyscale, :grayscale
101
-
102
-
103
-
104
- def flip
105
- img = @img.flip
106
- Image.new( img.width, img.height, img )
107
- end
108
- alias_method :flip_horizontally, :flip
109
-
110
- def mirror
111
- img = @img.mirror
112
- Image.new( img.width, img.height, img )
113
- end
114
- alias_method :flip_vertically, :mirror
115
- alias_method :flop, :mirror
116
-
117
-
118
-
119
-
120
-
121
- ## add replace_colors alias too? - why? why not?
122
- def change_colors( color_map )
123
- color_map = _parse_color_map( color_map )
124
-
125
- img = @img.dup ## note: make a deep copy!!!
126
- _change_colors!( img, color_map )
127
-
128
- ## wrap into Pixelart::Image - lets you use zoom() and such
129
- Image.new( img.width, img.height, img )
130
- end
131
- alias_method :recolor, :change_colors
132
-
133
-
134
-
135
- ## predefined palette8bit color maps
136
- ## (grayscale to sepia/blue/false/etc.)
137
- ## - todo/check - keep "shortcut" convenience predefined map - why? why not?
138
- PALETTE8BIT = {
139
- sepia: Palette8bit::GRAYSCALE.zip( Palette8bit::SEPIA ).to_h,
140
- blue: Palette8bit::GRAYSCALE.zip( Palette8bit::BLUE ).to_h,
141
- false: Palette8bit::GRAYSCALE.zip( Palette8bit::FALSE ).to_h,
142
- }
143
-
144
- def change_palette8bit( palette )
145
- ## step 0: mapping from grayscale to new 8bit palette (256 colors)
146
- color_map = if palette.is_a?( String ) || palette.is_a?( Symbol )
147
- PALETTE8BIT[ palette.to_sym ]
148
- ## todo/fix: check for missing/undefined palette not found - why? why not?
149
- else
150
- ## make sure we have colors all in Integer not names, hex, etc.
151
- palette = _parse_colors( palette )
152
- Palette8bit::GRAYSCALE.zip( palette ).to_h
153
- end
154
-
155
- ## step 1: convert to grayscale (256 colors)
156
- img = @img.grayscale
157
- _change_colors!( img, color_map )
158
-
159
- ## wrap into Pixelart::Image - lets you use zoom() and such
160
- Image.new( img.width, img.height, img )
161
- end
162
- alias_method :change_palette256, :change_palette8bit
163
-
164
-
165
- ####
166
- ## private helpers
167
- def _parse_colors( colors )
168
- colors.map {|color| Color.parse( color ) }
169
- end
170
-
171
- def _parse_color_map( color_map )
172
- color_map.map do |k,v|
173
- [Color.parse(k), Color.parse(v)]
174
- end.to_h
175
- end
176
-
177
- def _change_colors!( img, color_map )
178
- img.width.times do |x|
179
- img.height.times do |y|
180
- color = img[x,y]
181
- new_color = color_map[color]
182
- img[x,y] = new_color if new_color
183
- end
184
- end
185
- end
186
-
187
-
188
-
189
-
190
- #####
191
- # (image) delegates
192
- ## todo/check: add some more??
193
- def save( path, constraints = {} )
194
- # step 1: make sure outdir exits
195
- outdir = File.dirname( path )
196
- FileUtils.mkdir_p( outdir ) unless Dir.exist?( outdir )
197
-
198
- # step 2: save
199
- @img.save( path, constraints )
200
- end
201
- alias_method :write, :save
202
-
203
-
204
- def compose!( other, x=0, y=0 )
205
- @img.compose!( other.image, x, y ) ## note: "unwrap" inner image ref
206
- end
207
- alias_method :paste!, :compose!
208
-
209
-
210
- def width() @img.width; end
211
- def height() @img.height; end
212
-
213
- def []( x, y ) @img[x,y]; end
214
- def []=( x, y, value ) @img[x,y]=value; end
215
-
216
- def pixels() @img.pixels; end
217
-
218
- ### todo/check: add colors() e.g. @img.pixels.uniq - why? why not?
219
-
220
-
221
- ## return image ref - use a different name - why? why not?
222
- ## change to to_image - why? why not?
223
- def image() @img; end
224
-
225
-
226
-
227
-
228
- ######
229
- # helpers
230
- def self.parse_pixels( pixels )
231
- data = []
232
- pixels.each_line do |line|
233
- line = line.strip
234
- next if line.start_with?( '#' ) || line.empty? ## note: allow comments and empty lines
235
-
236
- ## note: allow multiple spaces or tabs to separate pixel codes
237
- ## 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
238
- ## or
239
- data << line.split( /[ \t]+/)
240
- end
241
- data
242
- end
243
-
244
-
245
-
246
- def self.parse_colors( colors )
247
- if colors.is_a?( Array ) ## convenience shortcut
248
- ## note: always auto-add color 0 as pre-defined transparent - why? why not?
249
- h = { '0' => Color::TRANSPARENT }
250
- colors.each_with_index do |color, i|
251
- h[ (i+1).to_s ] = Color.parse( color )
252
- end
253
- h
254
- else ## assume hash table with color map
255
- ## convert into ChunkyPNG::Color
256
- colors.map do |key,color|
257
- ## always convert key to string why? why not? use symbol?
258
- [ key.to_s, Color.parse( color ) ]
259
- end.to_h
260
- end
261
- end
262
-
263
-
264
- end # class Image
265
- end # module Pixelart
266
-
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
+