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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -3
- data/Manifest.txt +3 -0
- data/README.md +290 -290
- data/Rakefile +32 -32
- data/lib/pixelart/base.rb +93 -86
- data/lib/pixelart/blur.rb +19 -19
- data/lib/pixelart/circle.rb +46 -46
- data/lib/pixelart/color.rb +131 -131
- data/lib/pixelart/composite.rb +154 -154
- data/lib/pixelart/generator.rb +202 -0
- data/lib/pixelart/gradient.rb +106 -106
- data/lib/pixelart/image.rb +283 -283
- data/lib/pixelart/led.rb +37 -37
- data/lib/pixelart/misc.rb +66 -66
- data/lib/pixelart/palette.rb +72 -72
- data/lib/pixelart/pixelator.rb +165 -165
- data/lib/pixelart/sample.rb +120 -0
- data/lib/pixelart/silhouette.rb +35 -35
- data/lib/pixelart/sketch.rb +69 -69
- data/lib/pixelart/spots.rb +146 -146
- data/lib/pixelart/stripes.rb +116 -0
- data/lib/pixelart/transparent.rb +60 -60
- data/lib/pixelart/ukraine.rb +20 -33
- data/lib/pixelart/vector.rb +163 -163
- data/lib/pixelart/version.rb +22 -22
- data/lib/pixelart.rb +12 -12
- metadata +9 -6
data/lib/pixelart/composite.rb
CHANGED
@@ -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
|