pixelart 1.2.3 → 1.3.2

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.
@@ -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
+