pixelart 1.2.3 → 1.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,202 @@
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
+ def generate_image( *values, background: nil, before: nil )
174
+ ## note: generate_image NO longer supports
175
+ ## - generate by integer number (indexes), sorry
176
+
177
+ recs = to_recs( *values )
178
+
179
+ ## note: first construct/generate image on transparent background
180
+ ## add background if present as LAST step
181
+ img = Image.new( @width, @height )
182
+
183
+ recs.each do |rec|
184
+ ## note: before call(back) MUST change image INPLACE!!!!
185
+ before.call( img, rec ) if before
186
+ img.compose!( @sheet[ rec.id ] )
187
+ end
188
+
189
+ if background ## for now assume background is (simply) color
190
+ img2 = Image.new( @width, @height )
191
+ img2.compose!( Image.new( @width, @height, background ) )
192
+ img2.compose!( img )
193
+ img = img2
194
+ end
195
+
196
+ img
197
+ end
198
+ alias_method :generate, :generate_image
199
+
200
+ end # class Generator
201
+
202
+ end # module Pixelart