#!/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 end class Color attr_reader :red, :green, :blue def initialize(red:, green:, blue:) @red = red @green = green @blue = blue end WHITE = new(red: 15, green: 15, blue: 15) BLACK = new(red: 0, green: 0, blue: 0) def ==(other) red == other.red && green == other.green && blue == other.blue end def -(other) red - other.red + green - other.green + blue - other.blue end 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 def initialize @row = [] end def add(pixel:, x:) @row[x] = pixel end def colors_with_usage @row.find_all(&:color?).each_with_object({}) do |pixel, obj| obj[pixel.color] ||= 0 obj[pixel.color] += 1 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 prioritized_colors = rows[50].colors_with_usage.to_a.sort { |a, b| b.last <=> a.last } presets_by_pixel_color = [ nil, Color::BLACK, Color::WHITE ] unavailable_bitplane_colors = { 0 => true, 1 => true, 2 => true } MAX_BITPLANE_COLORS = 8 bitplane_colors = [ nil, Color::BLACK, Color::WHITE ] color_to_pixel_map = {} # take 5 + BLACK + WHITE colors into bitplane_colors prioritized_colors.map(&:first).each do |color| maybe_preset_index = presets_by_pixel_color.find_index { |c| c && color == c } if !maybe_preset_index.nil? bitplane_colors[maybe_preset_index] = color color_to_pixel_map[color] = maybe_preset_index unavailable_bitplane_colors[maybe_preset_index] = true elsif bitplane_colors.count < MAX_BITPLANE_COLORS next_available_bitplane_color = 8.times.find { |i| !unavailable_bitplane_colors[i] } bitplane_colors[next_available_bitplane_color] = color color_to_pixel_map[color] = next_available_bitplane_color unavailable_bitplane_colors[next_available_bitplane_color] = true else closest_match = bitplane_colors.each_with_index.map do |bp_color, index| next unless bp_color [(bp_color - color).abs, index] end.compact.min_by(&:first) pp color pp closest_match pp bitplane_colors[closest_match.last] puts '************************' color_to_pixel_map[color] = closest_match.last end end pp prioritized_colors pp bitplane_colors pp color_to_pixel_map # .to_a.sort(&:last).reverse