pixelart 1.2.2 → 1.3.1

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,154 +1,154 @@
1
- module Pixelart
2
-
3
- class ImageComposite < Image # check: (re)name to Collage, Sheet, Sprites, or such?
4
-
5
- ## default tile width / height in pixel -- check: (re)name to sprite or such? why? why not?
6
- TILE_WIDTH = 24
7
- TILE_HEIGHT = 24
8
-
9
-
10
- def self.read( path, width: TILE_WIDTH, height: TILE_WIDTH ) ## convenience helper
11
- img = ChunkyPNG::Image.from_file( path )
12
- new( img, width: width,
13
- height: width )
14
- end
15
-
16
-
17
- def initialize( *args, **kwargs )
18
- @tile_width = kwargs[:width] || kwargs[:tile_width] || TILE_WIDTH
19
- @tile_height = kwargs[:height] || kwargs[:tile_height] || TILE_HEIGHT
20
-
21
- ## check for background
22
- background = kwargs[:background] || kwargs[:tile_background]
23
-
24
- if background
25
- ## wrap into an array if not already an array
26
- ## and convert all colors to true rgba colors as integer numbers
27
- background = [background] unless background.is_a?( Array )
28
- @background_colors = background.map { |color| Color.parse( color ) }
29
- else
30
- ## todo/check: use empty array instead of nil - why? why not?
31
- @background_colors = nil
32
- end
33
-
34
-
35
- ## todo/fix: check type - args[0] is Image!!!
36
- if args.size == 1 ## assume "copy" c'tor with passed in image
37
- img = args[0] ## pass image through as-is
38
-
39
- @tile_cols = img.width / @tile_width ## e.g. 2400/24 = 100
40
- @tile_rows = img.height / @tile_height ## e.g. 2400/24 = 100
41
- @tile_count = @tile_cols * @tile_rows ## ## 10000 = 100x100 (2400x2400 pixel)
42
- elsif args.size == 2 || args.size == 0 ## cols, rows
43
- ## todo/fix: check type - args[0] & args[1] is Integer!!!!!
44
- ## todo/check - find a better name for cols/rows - why? why not?
45
- @tile_cols = args[0] || 3
46
- @tile_rows = args[1] || 3
47
- @tile_count = 0 # (track) current index (of added images)
48
-
49
- background_color = if @background_colors && @background_colors.size == 1
50
- @background_colors[0]
51
- else
52
- 0 # note: 0 is transparent (0) true color
53
- end
54
-
55
- ## todo/check - always auto-fill complete image (even if empty/no tiles)
56
- ## with background color if only one background color
57
- ## why? why not??? or always follow the "model"
58
- ## with more than one background color???
59
- img = ChunkyPNG::Image.new( @tile_cols * @tile_width,
60
- @tile_rows * @tile_height,
61
- background_color )
62
-
63
- else
64
- raise ArgumentError, "cols, rows or image arguments expected; got: #{args.inspect}"
65
- end
66
-
67
-
68
- puts " #{img.height}x#{img.width} (height x width)"
69
-
70
- super( nil, nil, img )
71
- end
72
-
73
-
74
- def count() @tile_count; end
75
- alias_method :size, :count ## add size alias (confusing if starting with 0?) - why? why not?
76
- alias_method :tile_count, :count
77
-
78
- def tile_width() @tile_width; end
79
- def tile_height() @tile_height; end
80
-
81
-
82
-
83
- #####
84
- # set / add tile
85
- def _add( image )
86
- y, x = @tile_count.divmod( @tile_cols )
87
-
88
- puts " [#{@tile_count}] @ (#{x}/#{y}) #{image.width}x#{image.height} (height x width)"
89
-
90
- ## note: only used if more than one background color specified
91
- ## needs to cycle through
92
- if @background_colors && @background_colors.size > 1
93
- i = x + y*@tile_cols
94
-
95
- ## note: cycle through background color for now
96
- background_color = @background_colors[i % @background_colors.size]
97
- background = Image.new( @tile_width, @tile_height, background_color ) ## todo/chekc: use "raw" ChunkyPNG:Image here - why? why not?
98
- background.compose!( image )
99
- image = background ## switch - make image with background new image
100
- end
101
-
102
- ## note: image.image - "unwrap" the "raw" ChunkyPNG::Image
103
- @img.compose!( image.image, x*@tile_width, y*@tile_height )
104
- @tile_count += 1
105
- end
106
-
107
- def add( image_or_images ) ## note: allow adding of image OR array of images
108
- if image_or_images.is_a?( Array )
109
- images = image_or_images
110
- images.each { |image| _add( image ) }
111
- else
112
- image = image_or_images
113
- _add( image )
114
- end
115
- end
116
- alias_method :<<, :add
117
-
118
-
119
-
120
- ######
121
- # get tile
122
-
123
- def tile( index )
124
- y, x = index.divmod( @tile_cols )
125
- img = @img.crop( x*@tile_width, y*@tile_height, @tile_width, @tile_height )
126
- Image.new( img.width, img.height, img ) ## wrap in pixelart image
127
- end
128
-
129
- def []( *args ) ## overload - why? why not?
130
- if args.size == 1
131
- index = args[0]
132
- tile( index )
133
- else
134
- super ## e.g [x,y] --- get pixel
135
- end
136
- end
137
-
138
-
139
- ## convenience helpers to loop over composite
140
- def each( &block )
141
- count.times do |i|
142
- block.call( tile( i ) )
143
- end
144
- end
145
-
146
- def each_with_index( &block )
147
- count.times do |i|
148
- block.call( tile( i ), i )
149
- end
150
- end
151
-
152
-
153
- end # class ImageComposite
154
- end # module Pixelart
1
+ module Pixelart
2
+
3
+ class ImageComposite < Image # check: (re)name to Collage, Sheet, Sprites, or such?
4
+
5
+ ## default tile width / height in pixel -- check: (re)name to sprite or such? why? why not?
6
+ TILE_WIDTH = 24
7
+ TILE_HEIGHT = 24
8
+
9
+
10
+ def self.read( path, width: TILE_WIDTH, height: TILE_WIDTH ) ## convenience helper
11
+ img = ChunkyPNG::Image.from_file( path )
12
+ new( img, width: width,
13
+ height: width )
14
+ end
15
+
16
+
17
+ def initialize( *args, **kwargs )
18
+ @tile_width = kwargs[:width] || kwargs[:tile_width] || TILE_WIDTH
19
+ @tile_height = kwargs[:height] || kwargs[:tile_height] || TILE_HEIGHT
20
+
21
+ ## check for background
22
+ background = kwargs[:background] || kwargs[:tile_background]
23
+
24
+ if background
25
+ ## wrap into an array if not already an array
26
+ ## and convert all colors to true rgba colors as integer numbers
27
+ background = [background] unless background.is_a?( Array )
28
+ @background_colors = background.map { |color| Color.parse( color ) }
29
+ else
30
+ ## todo/check: use empty array instead of nil - why? why not?
31
+ @background_colors = nil
32
+ end
33
+
34
+
35
+ ## todo/fix: check type - args[0] is Image!!!
36
+ if args.size == 1 ## assume "copy" c'tor with passed in image
37
+ img = args[0] ## pass image through as-is
38
+
39
+ @tile_cols = img.width / @tile_width ## e.g. 2400/24 = 100
40
+ @tile_rows = img.height / @tile_height ## e.g. 2400/24 = 100
41
+ @tile_count = @tile_cols * @tile_rows ## ## 10000 = 100x100 (2400x2400 pixel)
42
+ elsif args.size == 2 || args.size == 0 ## cols, rows
43
+ ## todo/fix: check type - args[0] & args[1] is Integer!!!!!
44
+ ## todo/check - find a better name for cols/rows - why? why not?
45
+ @tile_cols = args[0] || 3
46
+ @tile_rows = args[1] || 3
47
+ @tile_count = 0 # (track) current index (of added images)
48
+
49
+ background_color = if @background_colors && @background_colors.size == 1
50
+ @background_colors[0]
51
+ else
52
+ 0 # note: 0 is transparent (0) true color
53
+ end
54
+
55
+ ## todo/check - always auto-fill complete image (even if empty/no tiles)
56
+ ## with background color if only one background color
57
+ ## why? why not??? or always follow the "model"
58
+ ## with more than one background color???
59
+ img = ChunkyPNG::Image.new( @tile_cols * @tile_width,
60
+ @tile_rows * @tile_height,
61
+ background_color )
62
+
63
+ else
64
+ raise ArgumentError, "cols, rows or image arguments expected; got: #{args.inspect}"
65
+ end
66
+
67
+
68
+ puts " #{img.height}x#{img.width} (height x width)"
69
+
70
+ super( nil, nil, img )
71
+ end
72
+
73
+
74
+ def count() @tile_count; end
75
+ alias_method :size, :count ## add size alias (confusing if starting with 0?) - why? why not?
76
+ alias_method :tile_count, :count
77
+
78
+ def tile_width() @tile_width; end
79
+ def tile_height() @tile_height; end
80
+
81
+
82
+
83
+ #####
84
+ # set / add tile
85
+ def _add( image )
86
+ y, x = @tile_count.divmod( @tile_cols )
87
+
88
+ puts " [#{@tile_count}] @ (#{x}/#{y}) #{image.width}x#{image.height} (height x width)"
89
+
90
+ ## note: only used if more than one background color specified
91
+ ## needs to cycle through
92
+ if @background_colors && @background_colors.size > 1
93
+ i = x + y*@tile_cols
94
+
95
+ ## note: cycle through background color for now
96
+ background_color = @background_colors[i % @background_colors.size]
97
+ background = Image.new( @tile_width, @tile_height, background_color ) ## todo/chekc: use "raw" ChunkyPNG:Image here - why? why not?
98
+ background.compose!( image )
99
+ image = background ## switch - make image with background new image
100
+ end
101
+
102
+ ## note: image.image - "unwrap" the "raw" ChunkyPNG::Image
103
+ @img.compose!( image.image, x*@tile_width, y*@tile_height )
104
+ @tile_count += 1
105
+ end
106
+
107
+ def add( image_or_images ) ## note: allow adding of image OR array of images
108
+ if image_or_images.is_a?( Array )
109
+ images = image_or_images
110
+ images.each { |image| _add( image ) }
111
+ else
112
+ image = image_or_images
113
+ _add( image )
114
+ end
115
+ end
116
+ alias_method :<<, :add
117
+
118
+
119
+
120
+ ######
121
+ # get tile
122
+
123
+ def tile( index )
124
+ y, x = index.divmod( @tile_cols )
125
+ img = @img.crop( x*@tile_width, y*@tile_height, @tile_width, @tile_height )
126
+ Image.new( img.width, img.height, img ) ## wrap in pixelart image
127
+ end
128
+
129
+ def []( *args ) ## overload - why? why not?
130
+ if args.size == 1
131
+ index = args[0]
132
+ tile( index )
133
+ else
134
+ super ## e.g [x,y] --- get pixel
135
+ end
136
+ end
137
+
138
+
139
+ ## convenience helpers to loop over composite
140
+ def each( &block )
141
+ count.times do |i|
142
+ block.call( tile( i ) )
143
+ end
144
+ end
145
+
146
+ def each_with_index( &block )
147
+ count.times do |i|
148
+ block.call( tile( i ), i )
149
+ end
150
+ end
151
+
152
+
153
+ end # class ImageComposite
154
+ end # module Pixelart
@@ -0,0 +1,199 @@
1
+ ####
2
+ # "simple" generator (no different sizes, genders, etc.)
3
+ # uses built-in spritesheet for (archetypes &) attributes
4
+
5
+
6
+ module Pixelart
7
+
8
+ class Metadata
9
+ class Sprite
10
+ attr_reader :id, :name, :type, :more_names
11
+
12
+ def initialize( id:,
13
+ name:,
14
+ type:,
15
+ more_names: [] )
16
+ @id = id # zero-based index eg. 0,1,2,3, etc.
17
+ @name = name
18
+ @type = type
19
+ @more_names = more_names
20
+ end
21
+ end # class Metadata::Sprite
22
+ end # class Metadata
23
+
24
+
25
+
26
+
27
+ class Generator
28
+
29
+ ######
30
+ # static helpers - (turn into "true" static self.class methods - why? why not?)
31
+ #
32
+ def self.normalize_key( str )
33
+ ## add & e.g. B&W
34
+ ## add ' e.g. McDonald's Red
35
+ str.downcase.gsub(/[ ()&°'_-]/, '').strip
36
+ end
37
+
38
+ def self.normalize_name( str )
39
+ ## normalize spaces in more names
40
+ str.strip.gsub( /[ ]{2,}/, ' ' )
41
+ end
42
+
43
+ def normalize_key( str ) self.class.normalize_key( str ); end
44
+ def normalize_name( str ) self.class.normalize_name( str ); end
45
+
46
+
47
+
48
+ def build_attributes_by_name( recs )
49
+ h = {}
50
+ recs.each_with_index do |rec|
51
+ names = [rec.name] + rec.more_names
52
+
53
+ names.each do |name|
54
+ key = normalize_key( name )
55
+
56
+ if h[ key ]
57
+ puts "!!! ERROR - attribute name is not unique:"
58
+ pp rec
59
+ puts "duplicate:"
60
+ pp h[key]
61
+ exit 1
62
+ end
63
+ h[ key ] = rec
64
+ end
65
+ end
66
+ ## pp h
67
+ h
68
+ end
69
+
70
+
71
+ def build_recs( recs ) ## build and normalize (meta data) records
72
+ ## sort by id
73
+ recs = recs.sort do |l,r|
74
+ l['id'].to_i( 10 ) <=> r['id'].to_i( 10 ) # use base10 (decimal)
75
+ end
76
+
77
+ ## assert all recs are in order by id (0 to size)
78
+ recs.each_with_index do |rec, exp_id|
79
+ id = rec['id'].to_i(10)
80
+ if id != exp_id
81
+ puts "!! ERROR - meta data record ids out-of-order - expected id #{exp_id}; got #{id}"
82
+ exit 1
83
+ end
84
+ end
85
+
86
+ ## convert to "wrapped / immutable" kind-of struct
87
+ recs = recs.map do |rec|
88
+ id = rec['id'].to_i( 10 )
89
+ name = normalize_name( rec['name'] )
90
+ type = rec['type']
91
+
92
+ more_names = (rec['more_names'] || '').split( '|' )
93
+ more_names = more_names.map {|str| normalize_name( str ) }
94
+
95
+ Metadata::Sprite.new(
96
+ id: id,
97
+ name: name,
98
+ type: type,
99
+ more_names: more_names )
100
+ end
101
+ recs
102
+ end # method build_recs
103
+
104
+
105
+
106
+ def initialize( image_path="./spritesheet.png",
107
+ meta_path="./spritesheet.csv",
108
+ width: 24,
109
+ height: 24 )
110
+ @width = width
111
+ @height = height
112
+
113
+ @sheet = ImageComposite.read( image_path, width: @width, height: @height )
114
+ recs = CsvHash.read( meta_path )
115
+
116
+ @recs = build_recs( recs )
117
+
118
+ ## lookup by "case/space-insensitive" name / key
119
+ @attributes_by_name = build_attributes_by_name( @recs )
120
+ end
121
+
122
+
123
+ def spritesheet() @sheet; end
124
+ alias_method :sheet, :spritesheet
125
+
126
+
127
+ def records() @recs; end
128
+ alias_method :meta, :records
129
+
130
+
131
+
132
+
133
+ def find_meta( q )
134
+ key = normalize_key( q ) ## normalize q(uery) string/symbol
135
+
136
+ rec = @attributes_by_name[ key ]
137
+ if rec
138
+ puts " lookup >#{key}< => #{rec.id}: #{rec.name} / #{rec.type}"
139
+ else
140
+ puts "!! WARN - no lookup found for key >#{key}<"
141
+ end
142
+
143
+ rec
144
+ end
145
+
146
+ def find( q )
147
+ rec = find_meta( q )
148
+
149
+ ## return image if record found
150
+ rec ? @sheet[ rec.id ] : nil
151
+ end
152
+
153
+
154
+ def to_recs( *values )
155
+ recs = []
156
+
157
+ attribute_names = values
158
+
159
+ attribute_names.each do |attribute_name|
160
+ attribute = find_meta( attribute_name )
161
+ if attribute.nil?
162
+ puts "!! ERROR - attribute >#{attribute_name}< not found; sorry"
163
+ exit 1
164
+ end
165
+ recs << attribute
166
+ end
167
+
168
+ recs
169
+ end
170
+
171
+
172
+
173
+
174
+ def generate_image( *values, background: nil )
175
+
176
+ ids = if values[0].is_a?( Integer ) ## assume integer number (indexes)
177
+ values
178
+ else ## assume strings (names)
179
+ to_recs( *values ).map { |rec| rec.id }
180
+ end
181
+
182
+
183
+ img = Image.new( @width, @height )
184
+
185
+ if background ## for now assume background is (simply) color
186
+ img.compose!( Image.new( @width, @height, background ) )
187
+ end
188
+
189
+ ids.each do |id|
190
+ img.compose!( @sheet[ id ] )
191
+ end
192
+
193
+ img
194
+ end
195
+ alias_method :generate, :generate_image
196
+
197
+ end # class Generator
198
+
199
+ end # module Pixelart