Class: Spriteous
- Inherits:
-
Object
- Object
- Spriteous
- Defined in:
- lib/spriteous.rb
Instance Method Summary collapse
-
#extract(opts = {}) ⇒ Object
Entry point for the traverse method defined above.
-
#initialize(data) ⇒ Spriteous
constructor
A new instance of Spriteous.
-
#traverse(pixel) ⇒ Object
A “reverse flood fill” algorithm for finding all of the pixels that belong to a given sprite.
Constructor Details
#initialize(data) ⇒ Spriteous
Returns a new instance of Spriteous.
5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# File 'lib/spriteous.rb', line 5 def initialize(data) # Allow for creation from either raw PNG data or a file. meth = data[1..3] == "PNG" ? :from_string : :from_file @img = ChunkyPNG::Image.send meth, data # Determine the sheet's background color by checking the last pixel. Obtain # the sheet's width, which is used to determine a pixel's neighbors. back, @w = @img.pixels.last, @img.width # The original image's pixels get modified in place to erase the background # color, They're duplicated because the algorithm relies on "erasing" found # pixels, and we eventually need the originals to construct the sprites. @pixels = @img.pixels.map! { |p| p == back ? 0 : p }.dup end |
Instance Method Details
#extract(opts = {}) ⇒ Object
Entry point for the traverse method defined above. Returns array of sprites (instances of ChunkyPNG::Image) if called without the argument. save_format should be a format string that specifies %d in some way, as this is used to generate the names of the ripped sprites. Example: ‘sprites/mario/%03d.png’
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/spriteous.rb', line 49 def extract(opts = {}) sprites = [] # Sprites are erased as they're found, so we're done when the entire image # is comprised of a single color value, 0 (transparent) in this case. while @pixels.uniq.size > 1 @found = [] # Find the next sprite (first non-transparent pixel) and traverse from it # until there is no contiguous direction to continue in. @found will then # contain all of the pixel indices for the current sprite. traverse @pixels.find_index { |p| p > 0 } # @found contains pixel indices, but now we need coordinate data. x, y = @found.map { |f| f % @w }, @found.map { |f| f / @w } # Determine the sprite's bounding box (top, left, width, height), crop it # from the original image, and push it to the collection unless a minimum # size is desired and the resulting sprite's dimensions don't satisfy it. box = [x.min, y.min, (w = x.max - x.min + 1), (h = y.max - y.min + 1)] sprites << @img.crop(*box) if w * h >= opts[:min_size].to_i end return sprites unless opts[:save_format] unless opts[:save_format][/%\d*d/] raise ArgumentError, "%d is required to properly save extracted sprites." end dir = opts[:save_format].split('/')[0..-2].join '/' FileUtils.mkdir_p dir unless dir.empty? sprites.each_with_index { |s, i| s.save opts[:save_format] % i } end |
#traverse(pixel) ⇒ Object
A “reverse flood fill” algorithm for finding all of the pixels that belong to a given sprite. Rather than finding and replacing background pixels, it looks for a contiguous path starting from the first non-transparent pixel.
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/spriteous.rb', line 23 def traverse(pixel) queue = [pixel] # Used for checking the target's adjacent pixels. All eight directions are # checked to avoid missing single diagonal pixels at the edges. neighbors = [-@w - 1, -@w, -@w + 1, -1, 1, @w - 1, @w, @w + 1] until queue.empty? p = queue.pop if @pixels[p] && @pixels[p] > 0 # Store this non-transparent pixel's index and then make it transparent # to avoid unnecessarily checking it again from a neighbor. @pixels[(@found << p).last] = 0 # Apply the eight cardinal directions to the current pixel's index and # prepare them to be checked on the next iteration. queue.concat neighbors.map { |n| p + n } end end end |