Class: Sabrina::Sprite

Inherits:
Bytestream show all
Defined in:
lib/sabrina/sprite.rb

Overview

A class tailored towards dealing with graphical (sprite) data.

Note that a sprite generated from ROM data does not contain color data by default. Pass a Palette as :palette in the option hash, or to #palette=, to specify a default palette for the RGB output methods.

Instance Attribute Summary collapse

Attributes inherited from Bytestream

#filename, #index, #last_write, #rom, #table, #work_dir

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Bytestream

#lz77_mode, #offset, #offset=, parse_offset, #pointer, #string_mode

Methods included from Bytestream::ByteInput

#from_bytes, #from_hex, #from_rom, #from_rom_as_lz77, #from_table, #from_table_as_pointer

Methods included from Bytestream::RomOperations

#calculate_length, #clear_cache, #reload_from_rom, #write_to_rom

Methods included from Bytestream::ByteOutput

#to_b, #to_bytes, #to_hex, #to_hex_reverse, #to_i, #to_lz77

Constructor Details

#initialize(h = {}) ⇒ Sprite

Same as Bytestream#initialize, but with :lz77 and :pointer_mode set to true by default and support for the following extra options.

Parameters:

  • h (Hash) (defaults to: {})

    @option h [Integer] :width The width of the picture. @option h [Palette] :palette The default palette to use

    with the RGB and Canvas output methods.
    

See Also:



120
121
122
123
124
125
126
127
128
# File 'lib/sabrina/sprite.rb', line 120

def initialize(h = {})
  @lz77 = true
  @pointer_mode = true

  @width = 64
  @palette = nil

  super
end

Instance Attribute Details

#paletteObject

Gets or sets the default palette for RGB output. Note that this will not cause the palette to also be automatically written to ROM on sprite Bytestream::RomOperations#write_to_rom.



11
12
13
# File 'lib/sabrina/sprite.rb', line 11

def palette
  @palette
end

Class Method Details

.from_canvas(c, palette = Palette.empty, h = {}) ⇒ Sprite

Generates a sprite from a Canvas and optionally attempts to match the colors to the provided Palette.

Parameters:

Returns:



66
67
68
# File 'lib/sabrina/sprite.rb', line 66

def from_canvas(c, palette = Palette.empty, h = {})
  from_rgb(c.to_rgb_stream, c.width, palette, h)
end

.from_png(file, palette = Palette.empty, h = {}) ⇒ Sprite

Generates a sprite from a PNG file and optionally attempts to match the colors to the provided Palette.

Internally, this creates a Canvas object from the provided file and then passes the extracted RGB stream and width to from_rgb along with the palette.

Parameters:

Returns:



51
52
53
54
55
# File 'lib/sabrina/sprite.rb', line 51

def from_png(file, palette = Palette.empty, h = {})
  c = ChunkyPNG::Canvas.from_file(file)

  from_canvas(c, palette, h)
end

.from_rgb(rgb, width = 64, palette = Palette.empty, h = {}) ⇒ Sprite

Generates a sprite from a stream of bytes following the 0xRRGGBB format with the provided width, optionally matching the colors to the supplied Palette (and failing if the sprite dimensions are not multiples of 8 or the total number of colors in the image and the palette exceeds 16).

It is important to remember that while the resulting image will be ready for saving to PNG, writing it to a ROM will not save the color data by itself. The generated palette (accessible via #palette) should be written separately. The Spritesheet plugin should take care of that for you.

Parameters:

  • rgb (String)
  • width (Integer) (defaults to: 64)
  • palette (Palette) (defaults to: Palette.empty)
  • h (Hash) (defaults to: {})

Returns:



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/sabrina/sprite.rb', line 87

def from_rgb(rgb, width = 64, palette = Palette.empty, h = {})
  fail 'RGB stream length must divide by 3.' unless rgb.length % 3 == 0

  unless width % 8 == 0 && (rgb.length / 3 / width) % 8 == 0
    fail 'Sprite dimensions must be divisible by 8.'
  end

  out_array = []

  until rgb.empty?
    pixel = rgb.slice!(0, 3).unpack('CCC')
    palette.add(pixel) unless palette.index_of(pixel)
    out_array << palette.index_of(pixel).to_s(16).upcase
  end

  h.merge!(
    representation: out_array,
    width: width,
    palette: palette
  )
  new(h)
end

.from_rom(rom, offset, width = 64, h = {}) ⇒ Sprite

Same as Bytestream::ByteInput#from_rom, but supplies no length (due to implicit Lz77 mode) and supports a width parameter for the resulting picture.

Parameters:

Returns:

See Also:



34
35
36
37
# File 'lib/sabrina/sprite.rb', line 34

def from_rom(rom, offset, width = 64, h = {})
  h.merge!(width: width)
  super(rom, offset, nil, h)
end

.from_table(rom, table, index, width = 64, h = {}) ⇒ Sprite

Same as Bytestream::ByteInput#from_table_as_pointer, but also supports a width parameter for the resulting picture.

Parameters:

Returns:

See Also:



21
22
23
24
# File 'lib/sabrina/sprite.rb', line 21

def from_table(rom, table, index, width = 64, h = {})
  h.merge!(width: width)
  from_table_as_pointer(rom, table, index, h)
end

Instance Method Details

#generate_bytesString

TODO:

Some breakage with number 360, is this the culprit?

Converts the internal representation to a GBA-compatible stream of bytes.

Returns:

  • (String)

See Also:



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/sabrina/sprite.rb', line 168

def generate_bytes
  in_array = []
  present.join('').scan(/(.)(.)/) { |x, y| in_array += [y, x] }

  column_num = @width / 8
  out_array = []

  loop do
    break if in_array.empty?
    columns = [[]] * column_num

    until columns[0].length == 8 * 8
      column_num.times { |i| columns[i] += in_array.slice!(0, 8) }
    end # Filled one block

    out_array += columns.slice!(0) until columns.empty?
  end

  Bytestream.from_hex(out_array.join('')).to_b
end

#justifyself

Crops or repeats the sprite vertically until it meets the current ROM’s frame count, assuming 64x64 pixels per frame.

Returns:

  • (self)


212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/sabrina/sprite.rb', line 212

def justify
  frame_count =
    if @table.to_sym == :front_table
      @rom.special_frames.fetch(@index, @rom.frames).first
    else
      @rom.special_frames.fetch(@index, @rom.frames).last
    end

  h = { lz77: false }
  target = frame_count * 64 * 64

  old_rep = @representation.dup
  if @representation.length < target
    @representation += old_rep until @representation.length >= target
    h = { lz77: true }
  elsif @representation.length > target
    @representation.slice!(0, target)
    h = { lz77: true }
  end

  clear_cache(h)
end

#presentArray Also known as: to_a

Returns an array of characters that represent the palette index of each pixel in hexadecimal format.

Returns:

  • (Array)

    an array of characters from 0 through F.



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/sabrina/sprite.rb', line 134

def present
  return @representation if @representation
  # return nil if to_bytes.empty?

  in_array = []
  to_hex.scan(/(.)(.)/) { |x, y| in_array += [y, x] }

  column_num = @width / 8

  blocks = []
  blocks << in_array.slice!(0, 64) until in_array.empty?

  out_array = []
  i = 0
  loop do
    loop do
      break if blocks[i % column_num].empty?
      out_array += blocks[i % column_num].slice!(0, 8)
      i += 1
    end
    blocks.slice!(0, column_num)
    break if blocks.empty?
  end

  @representation = out_array
end

#rom=(p_rom) ⇒ Object

See Also:



236
237
238
239
240
# File 'lib/sabrina/sprite.rb', line 236

def rom=(p_rom)
  @rom = p_rom
  justify
  @rom
end

#to_ascii(width_multiplier = 2) ⇒ String

Outputs the sprite as ASCII art with each pixel represented by the hexadecimal value of its palette index.

Parameters:

  • width_multiplier (Integer) (defaults to: 2)

    each pixel will be repeated this many times horizontally. Defaults to 2 for better proportions with typical monospace fonts.

Returns:

  • (String)

See Also:



287
288
289
290
291
292
293
294
# File 'lib/sabrina/sprite.rb', line 287

def to_ascii(width_multiplier = 2)
  output = ''
  present.each_index do |i|
    output << present[i] * width_multiplier
    output << "\n" if (i + 1) % @width == 0
  end
  output
end

#to_canvas(pal = @palette) ⇒ Canvas

Converts the internal representation to a Canvas object using the default palette or the provided one (and failing if neither is present.)

Parameters:

  • pal (Palette) (defaults to: @palette)

Returns:

  • (Canvas)

    a Canvas object.

See Also:



252
253
254
255
256
257
258
# File 'lib/sabrina/sprite.rb', line 252

def to_canvas(pal = @palette)
  ChunkyPNG::Canvas.from_rgb_stream(
    @width,
    present.length / @width,
    to_rgb(pal)
  )
end

#to_png(file, dir = '', pal = @palette) ⇒ Object Also known as: to_file

Saves the internal representation to a PNG file (appending the file extension if absent) using the default palette or the provided one (and failing if neither is present.)

Parameters:

  • file (String)

    the file to save to. A .png extension is optional.

  • pal (Palette) (defaults to: @palette)

See Also:



267
268
269
270
271
272
273
274
275
# File 'lib/sabrina/sprite.rb', line 267

def to_png(file, dir = '', pal = @palette)
  dir << '/' unless dir.empty? || dir.end_with?('/')
  FileUtils.mkpath(dir) unless Dir.exist?(dir)

  path = dir << file
  path << '.png' unless path.downcase.end_with?('.png')

  to_canvas(pal).save(path)
end

#to_rgb(pal = @palette) ⇒ String

Converts the internal representation to a stream of 0xRRGGBB bytes using the default palette or the provided one (and failing if neither is present.)

Parameters:

  • pal (Palette) (defaults to: @palette)

Returns:

  • (String)

See Also:



196
197
198
199
200
201
202
203
204
205
206
# File 'lib/sabrina/sprite.rb', line 196

def to_rgb(pal = @palette)
  fail 'A palette must be specified for conversion to RGB.' unless pal

  rgb = present.map do |x|
    color = pal.present[x.hex]
    fail "No such entry in the palette: #{x.hex}" unless color
    color.map(&:chr).join('')
  end

  rgb.join('')
end

#to_sString

A blurb showing the sprite dimensions.

Returns:

  • (String)


299
300
301
# File 'lib/sabrina/sprite.rb', line 299

def to_s
  "Sprite (#{ @width }x#{ present.length / @width })"
end