2024-06-03 16:46:32 +00:00
|
|
|
#!/usr/bin/env ruby
|
|
|
|
|
|
|
|
require 'rmagick'
|
|
|
|
|
|
|
|
# [ ] load the image
|
|
|
|
# [ ] map each pixel to a 12 bit color, with pixels less than 25% transparent as mask
|
|
|
|
# [ ] sort the color counts
|
|
|
|
# [ ] transparent pixels become the mask
|
|
|
|
# [ ] turn the big color counts into three bitplames worth of data
|
|
|
|
# [ ] turn the little color counts into up to 8 sprites worth of data strips & color changes
|
|
|
|
|
|
|
|
image = Magick::Image.read('topaz.png').first
|
|
|
|
|
|
|
|
rows = []
|
|
|
|
|
|
|
|
class MaskPixel
|
|
|
|
def initialize; end
|
|
|
|
|
|
|
|
def color? = false
|
2024-06-11 02:04:40 +00:00
|
|
|
|
|
|
|
def ==(other)
|
|
|
|
other.is_a?(MaskPixel)
|
|
|
|
end
|
2024-06-03 16:46:32 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
class Color
|
|
|
|
attr_reader :red, :green, :blue
|
|
|
|
|
|
|
|
def initialize(red:, green:, blue:)
|
|
|
|
@red = red
|
|
|
|
@green = green
|
|
|
|
@blue = blue
|
|
|
|
end
|
|
|
|
|
2024-06-04 16:44:12 +00:00
|
|
|
WHITE = new(red: 15, green: 15, blue: 15)
|
|
|
|
BLACK = new(red: 0, green: 0, blue: 0)
|
2024-06-11 02:04:40 +00:00
|
|
|
TRANSPARENT = new(red: -1, green: -1, blue: -1)
|
2024-06-04 16:44:12 +00:00
|
|
|
|
2024-06-03 16:46:32 +00:00
|
|
|
def ==(other)
|
|
|
|
red == other.red && green == other.green && blue == other.blue
|
|
|
|
end
|
|
|
|
|
2024-06-11 02:04:40 +00:00
|
|
|
def transparent?
|
|
|
|
red == -1 && green == -1 && blue == -1
|
|
|
|
end
|
|
|
|
|
2024-06-04 16:44:12 +00:00
|
|
|
def -(other)
|
|
|
|
red - other.red + green - other.green + blue - other.blue
|
|
|
|
end
|
|
|
|
|
2024-06-11 02:04:40 +00:00
|
|
|
def for_diff
|
|
|
|
2 * red * red + 4 * green * green + 3 * blue * blue
|
|
|
|
end
|
|
|
|
|
2024-06-03 16:46:32 +00:00
|
|
|
alias eql? ==
|
|
|
|
|
|
|
|
def hash
|
|
|
|
red * 256 + green * 16 + blue
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class BitplanePixel
|
|
|
|
attr_reader :color
|
|
|
|
|
|
|
|
def initialize(color:)
|
|
|
|
@color = color
|
|
|
|
end
|
|
|
|
|
|
|
|
def color? = true
|
|
|
|
end
|
|
|
|
|
|
|
|
MAX_ALPHA = Magick::QuantumRange * 0.25
|
|
|
|
BIT_SHIFT = Math.log2(Magick::QuantumRange + 1) - 4
|
|
|
|
|
|
|
|
class PixelRow
|
2024-06-11 02:04:40 +00:00
|
|
|
include Enumerable
|
|
|
|
|
2024-06-03 16:46:32 +00:00
|
|
|
def initialize
|
|
|
|
@row = []
|
|
|
|
end
|
|
|
|
|
|
|
|
def add(pixel:, x:)
|
|
|
|
@row[x] = pixel
|
|
|
|
end
|
|
|
|
|
2024-06-11 02:04:40 +00:00
|
|
|
def each(&block)
|
|
|
|
@row.each(&block)
|
|
|
|
end
|
|
|
|
|
|
|
|
def prioritized_colors
|
|
|
|
colors_with_usage.sort_by(&:last).reverse.map(&:first)
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2024-06-03 16:46:32 +00:00
|
|
|
def colors_with_usage
|
2024-06-11 02:04:40 +00:00
|
|
|
last = nil
|
|
|
|
count = 0
|
|
|
|
|
2024-06-03 16:46:32 +00:00
|
|
|
@row.find_all(&:color?).each_with_object({}) do |pixel, obj|
|
2024-06-11 02:04:40 +00:00
|
|
|
if last != pixel.color
|
|
|
|
last = pixel.color
|
|
|
|
count = 1
|
|
|
|
end
|
|
|
|
|
|
|
|
count += count
|
|
|
|
|
2024-06-03 16:46:32 +00:00
|
|
|
obj[pixel.color] ||= 0
|
2024-06-11 02:04:40 +00:00
|
|
|
obj[pixel.color] += count
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class RowColors
|
|
|
|
def initialize; end
|
|
|
|
end
|
|
|
|
|
|
|
|
class RowColorCalculator
|
|
|
|
MAX_BITPLANE_COLORS = 8
|
|
|
|
MAX_SPRITE_COLORS = 8
|
|
|
|
|
|
|
|
def self.calculate(row:)
|
|
|
|
prioritized_colors = row.prioritized_colors
|
|
|
|
|
|
|
|
bitplane_pixels_by_color = {
|
|
|
|
Color::TRANSPARENT => 0,
|
|
|
|
Color::BLACK => 1,
|
|
|
|
Color::WHITE => 2
|
|
|
|
}
|
|
|
|
|
|
|
|
sprite_pixels_by_color = {}
|
|
|
|
|
|
|
|
colors_by_bitplane_pixel = {}
|
|
|
|
colors_by_sprite_pixel = {}
|
|
|
|
|
|
|
|
prioritized_colors.each do |color|
|
|
|
|
next if bitplane_pixels_by_color.keys.include?(color)
|
|
|
|
|
|
|
|
closest_matches = bitplane_pixels_by_color.keys.reject(&:transparent?).map do |bp_color|
|
|
|
|
[(bp_color.for_diff - color.for_diff).abs, bp_color]
|
|
|
|
end.compact
|
|
|
|
|
|
|
|
closest_match = closest_matches.min_by(&:first)
|
|
|
|
|
|
|
|
if bitplane_pixels_by_color.values.max == MAX_BITPLANE_COLORS - 1
|
|
|
|
bitplane_pixels_by_color[color] = bitplane_pixels_by_color[closest_match.last]
|
|
|
|
|
|
|
|
max_sprite_value = sprite_pixels_by_color.values.max
|
|
|
|
|
|
|
|
if !max_sprite_value || max_sprite_value < MAX_SPRITE_COLORS - 1
|
|
|
|
new_pixel_index = colors_by_sprite_pixel.count
|
|
|
|
sprite_pixels_by_color[color] = new_pixel_index
|
|
|
|
colors_by_sprite_pixel[new_pixel_index] = color
|
|
|
|
end
|
|
|
|
else
|
|
|
|
new_pixel_index = colors_by_bitplane_pixel.count + 3
|
|
|
|
bitplane_pixels_by_color[color] = new_pixel_index
|
|
|
|
colors_by_bitplane_pixel[new_pixel_index] = color
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
{
|
|
|
|
bitplane_pixels_by_color:,
|
|
|
|
sprite_pixels_by_color:,
|
|
|
|
colors_by_bitplane_pixel:,
|
|
|
|
colors_by_sprite_pixel:
|
|
|
|
}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class RowMaskBitplaneCalculator
|
|
|
|
def self.calculate(row:)
|
|
|
|
bits = row.map { |p| p.is_a?(MaskPixel) ? 0 : 1 }
|
|
|
|
|
|
|
|
bits.each_slice(8).map do |pixels|
|
|
|
|
byte = 0
|
|
|
|
|
|
|
|
pixels.each_with_index do |pixel, index|
|
|
|
|
bit = 7 - index
|
|
|
|
|
|
|
|
byte |= (pixel << bit)
|
|
|
|
end
|
|
|
|
|
|
|
|
byte
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class RowBitplaneCalculator
|
|
|
|
def self.calculate(row:, bitplane_pixels_by_color:)
|
|
|
|
output_pixels = row.map do |pixel|
|
|
|
|
next 0 if pixel.is_a?(MaskPixel)
|
|
|
|
|
|
|
|
bitplane_pixels_by_color[pixel.color] || 0
|
|
|
|
end
|
|
|
|
|
|
|
|
bitplanes = [[], [], []]
|
|
|
|
|
|
|
|
output_pixels.each_slice(8) do |pixels|
|
|
|
|
bits = [0] * 3
|
|
|
|
|
|
|
|
pixels.each_with_index do |pixel, index|
|
|
|
|
bit = 7 - index
|
|
|
|
|
|
|
|
3.times do |i|
|
|
|
|
bits[i] |= (((pixel >> i) & 1) << bit)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
3.times do |i|
|
|
|
|
bitplanes[i] << bits[i]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
bitplanes
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class Sprite
|
|
|
|
attr_reader :x, :y, :color, :pixels
|
|
|
|
|
|
|
|
# @!attribute color
|
|
|
|
# @return Color
|
|
|
|
|
|
|
|
def initialize(x:, y:, color:)
|
|
|
|
@x = x
|
|
|
|
@y = y
|
|
|
|
@color = color
|
|
|
|
@pixels = [0] * 16
|
|
|
|
end
|
|
|
|
|
|
|
|
def add_pixel(x:, index:)
|
|
|
|
return if x - @x >= 16
|
|
|
|
|
|
|
|
@pixels[x - @x] = index
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class RowSpriteCalculator
|
|
|
|
def self.calculate(y:, row:, sprite_pixels_by_color:)
|
|
|
|
sprites_by_color = {}
|
|
|
|
|
|
|
|
row.each_with_index.map do |pixel, x|
|
|
|
|
next if pixel.is_a?(MaskPixel)
|
|
|
|
|
|
|
|
sprite_pixel = sprite_pixels_by_color[pixel.color]
|
|
|
|
next unless sprite_pixel
|
|
|
|
|
|
|
|
sprite = sprites_by_color[pixel.color]
|
|
|
|
unless sprite
|
|
|
|
sprite = Sprite.new(y:, x:, color: pixel.color)
|
|
|
|
sprites_by_color[pixel.color] = sprite
|
|
|
|
end
|
|
|
|
|
|
|
|
sprite.add_pixel(x:, index: 1)
|
|
|
|
end
|
|
|
|
|
|
|
|
sprites_by_color
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
module Amiga
|
|
|
|
class Util
|
|
|
|
TOTAL_HEIGHT = 256
|
|
|
|
TOP_OFFSET = 44
|
|
|
|
LEFT_OFFSET = 128
|
|
|
|
|
|
|
|
SPRITE_HEIGHT = TOTAL_HEIGHT + TOP_OFFSET
|
|
|
|
|
|
|
|
def self.color_to_amiga_word(color:)
|
|
|
|
(color.blue + (color.green << 4) + (color.red << 8))
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.sprposctl(x:, y:)
|
|
|
|
y += TOP_OFFSET
|
|
|
|
x += LEFT_OFFSET
|
|
|
|
|
|
|
|
sprpos = ((y & 0xff) << 8) +
|
|
|
|
((x & 0x1fe) >> 1)
|
|
|
|
sprctl = (((y + TOTAL_HEIGHT) & 0xff) << 8) +
|
|
|
|
((y & 0x100) >> 6) +
|
|
|
|
(((y + TOTAL_HEIGHT) & 0x100) >> 7) +
|
|
|
|
(x & 1)
|
|
|
|
|
|
|
|
{ sprpos:, sprctl: }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class BitplaneCopperlistFactory
|
|
|
|
def self.build(colors_by_bitplane_pixel:)
|
|
|
|
5.times.map do |color_index|
|
|
|
|
color = colors_by_bitplane_pixel[color_index + 3] || Color::BLACK
|
|
|
|
|
|
|
|
Amiga::Util.color_to_amiga_word(color:)
|
|
|
|
end.pack('n*')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class BitplaneDataFactory
|
|
|
|
def self.build(bitplanes:)
|
|
|
|
bitplanes.map do |data|
|
|
|
|
data.pack('C*')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class BitplaneMaskDataFactory
|
|
|
|
def self.build(bitplane:)
|
|
|
|
bitplane.pack('C*')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class SpriteCopperlistFactory
|
|
|
|
# @param sprites [Array<Sprite>]
|
|
|
|
def self.build(sprites:, y_offset:)
|
|
|
|
obj = []
|
|
|
|
|
|
|
|
8.times do |sprite_index|
|
|
|
|
color = 0
|
|
|
|
sprpos = 0
|
|
|
|
sprctl = 0
|
|
|
|
|
|
|
|
sprite = sprites[sprite_index]
|
|
|
|
if sprite
|
|
|
|
result = Amiga::Util.sprposctl(x: sprite.x, y: sprite.y + y_offset)
|
|
|
|
|
|
|
|
color = Amiga::Util.color_to_amiga_word(color: sprite.color)
|
|
|
|
sprpos = result[:sprpos]
|
|
|
|
sprctl = result[:sprctl]
|
|
|
|
end
|
|
|
|
|
|
|
|
obj << color
|
|
|
|
obj << sprpos
|
|
|
|
obj << sprctl
|
|
|
|
end
|
|
|
|
|
|
|
|
obj.pack('n*')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class SpriteDataFactory
|
|
|
|
def self.build(sprites:)
|
|
|
|
sprite_out = [''] * 8
|
|
|
|
|
|
|
|
sprites_with_bitplane = sprites.zip([0, 1] * 4)
|
|
|
|
|
|
|
|
8.times do |sprite_index|
|
|
|
|
sprite, bitplane = sprites_with_bitplane[sprite_index]
|
|
|
|
|
|
|
|
2.times do |current_bitplane|
|
|
|
|
result = 0
|
|
|
|
|
|
|
|
if bitplane == current_bitplane
|
|
|
|
sprite.pixels.each_with_index do |pixel, bit|
|
|
|
|
result |= (pixel << (15 - bit))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
sprite_out[sprite_index] += [result].pack('n*')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
sprite_out
|
2024-06-03 16:46:32 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
image.each_pixel do |px, x, y|
|
|
|
|
rows[y] ||= PixelRow.new
|
|
|
|
|
|
|
|
pixel = if px.alpha < MAX_ALPHA
|
|
|
|
MaskPixel.new
|
|
|
|
else
|
|
|
|
BitplanePixel.new(
|
|
|
|
color: Color.new(
|
|
|
|
red: px.red >> BIT_SHIFT,
|
|
|
|
green: px.green >> BIT_SHIFT,
|
|
|
|
blue: px.blue >> BIT_SHIFT
|
|
|
|
)
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
rows[y].add(pixel:, x:)
|
|
|
|
end
|
|
|
|
|
2024-06-11 02:04:40 +00:00
|
|
|
copper_colors = ''
|
|
|
|
topaz_bitplanes = [''] * 3
|
|
|
|
sprite_copperlist = ''
|
|
|
|
sprite_data = [''] * 8
|
2024-06-04 16:44:12 +00:00
|
|
|
|
2024-06-11 02:04:40 +00:00
|
|
|
mask_bitplane = ''
|
2024-06-04 16:44:12 +00:00
|
|
|
|
2024-06-11 02:04:40 +00:00
|
|
|
8.times do |i|
|
|
|
|
result = Amiga::Util.sprposctl(x: 319, y: 0)
|
|
|
|
sprite_data[i] = [result[:sprpos], result[:sprctl]].pack('n*')
|
|
|
|
end
|
2024-06-04 16:44:12 +00:00
|
|
|
|
2024-06-11 02:04:40 +00:00
|
|
|
rows.each_with_index do |row, y_offset|
|
|
|
|
result = RowColorCalculator.calculate(row:)
|
2024-06-04 16:44:12 +00:00
|
|
|
|
2024-06-11 02:04:40 +00:00
|
|
|
bitplane_pixels_by_color = result[:bitplane_pixels_by_color]
|
|
|
|
sprite_pixels_by_color = result[:sprite_pixels_by_color]
|
|
|
|
colors_by_bitplane_pixel = result[:colors_by_bitplane_pixel]
|
2024-06-04 16:44:12 +00:00
|
|
|
|
2024-06-11 02:04:40 +00:00
|
|
|
bitplanes = RowBitplaneCalculator.calculate(row:, bitplane_pixels_by_color:)
|
|
|
|
sprites = RowSpriteCalculator.calculate(y: y_offset, row:, sprite_pixels_by_color:).values
|
2024-06-04 16:44:12 +00:00
|
|
|
|
2024-06-11 02:04:40 +00:00
|
|
|
mask = RowMaskBitplaneCalculator.calculate(row:)
|
2024-06-04 16:44:12 +00:00
|
|
|
|
2024-06-11 02:04:40 +00:00
|
|
|
mask_bitplane += Amiga::BitplaneMaskDataFactory.build(bitplane: mask)
|
2024-06-04 16:44:12 +00:00
|
|
|
|
2024-06-11 02:04:40 +00:00
|
|
|
copper_colors += Amiga::BitplaneCopperlistFactory.build(colors_by_bitplane_pixel:)
|
|
|
|
raw_bitplanes = Amiga::BitplaneDataFactory.build(bitplanes:)
|
2024-06-04 16:44:12 +00:00
|
|
|
|
2024-06-11 02:04:40 +00:00
|
|
|
sprite_copperlist += Amiga::SpriteCopperlistFactory.build(sprites:, y_offset:)
|
|
|
|
raw_sprite_data = Amiga::SpriteDataFactory.build(sprites:)
|
2024-06-04 16:44:12 +00:00
|
|
|
|
2024-06-11 02:04:40 +00:00
|
|
|
raw_bitplanes.each_with_index do |raw, index|
|
|
|
|
topaz_bitplanes[index] += raw
|
|
|
|
end
|
2024-06-04 16:44:12 +00:00
|
|
|
|
2024-06-11 02:04:40 +00:00
|
|
|
raw_sprite_data.each_with_index do |raw, index|
|
|
|
|
sprite_data[index] += raw
|
2024-06-04 16:44:12 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-06-11 02:04:40 +00:00
|
|
|
8.times do |i|
|
|
|
|
sprite_data[i] += [0, 0].pack('n*')
|
|
|
|
end
|
2024-06-04 16:44:12 +00:00
|
|
|
|
2024-06-11 02:04:40 +00:00
|
|
|
File.open('copper-colors', 'wb') { |fh| fh.print copper_colors }
|
|
|
|
File.open('topaz-bitplane', 'wb') { |fh| fh.print topaz_bitplanes.join('') }
|
|
|
|
File.open('mask-bitplane', 'wb') { |fh| fh.print mask_bitplane }
|
|
|
|
File.open('sprite-copperlist', 'wb') { |fh| fh.print sprite_copperlist }
|
|
|
|
File.open('sprite-data', 'wb') { |fh| fh.print sprite_data.join('') }
|