Rothko Gem Explained
A Breakdown of the Rothko Terminal Art Gem
I recently created a gem for Ruby called ‘rothko’ which takes in a PNG file and outputs a low-res image to the terminal. It’s still a work in progress, and it is a bit cryptic, so I’d like to explain how this:
Rothko::Drawing.new(‘FILEPATH’, width)
Can make this happen:
If you check out the story behind the gem you can get a better idea of why it looks so strange and how to get better looking images, but for now I’d like to break down what exactly is happening.
def initialize(path, width)
input = ChunkyPNG::Image.from_file(path)
@width = width
@img = input.resample_nearest_neighbor(width, get_height(input))
make_drawing
end
When initialized, we draw heavily on the ChunkyPNG gem to interpret the image we are passing in. The input variable is a new ChunkyPNG Image object, which allows us to resample the PNG down to our desired size. Since we are only drawing images in the 32x32 range, having to iterate over every pixel in a massive image would be unnecessary.
Note: in the above code “self.width has been switch to just width to fit on one line.
def make_drawing
ln_arr = create_color_string.scan(/.{#{self.width}}/)
ln_arr.each {|ln| draw_line(ln.split(""))}
end
Make drawing calls on two big methods. The first, create_color_string:
def create_color_string
(0...img.height).map do |y|
(0...img.width).map do |x|
pix = self.img[x,y]
pix_vals = [r(pix), g(pix), b(pix)]
find_closest_term_color(pix_vals)
end
end.join
end
We iterate over every picture in our shrunk-down image, using ChunkyPNG again to call on the pixels with the image[](x,y) function. Then, with the picture’s RGB values, we call on another method that uses Euclidian Distance to find the closest color from our limited palette. All of these colors are saved as a string — an example being “YgKKR” for a picture that has pixels of Yellow, Light Green, Black, Black and Red.
def find_closest_term_color(pixel_values)
color = ""
lil_dist = 195075
@@palette.each do |col_name, col_values|
dist = find_distance(col_values, pixel_values)
if dist < lil_dist
lil_dist = dist
color = col_name
end
end
color
end
It checks 16 times — once for each color in the palette, saving whichever has the closest distance. One of the sections in most dire need of refactoring!
def find_distance(color1, color2)
delta_r = color1[0] - color2[0];
delta_g = color1[1] - color2[1];
delta_b = color1[2] - color2[2];
delta_r * delta_r + delta_g * delta_g + delta_b * delta_b
end
And then finally, our make_drawing calls on draw_line to output the colors to the terminal:
def draw_line(pixels)
pix_line = ""
pixels.each do |pixel|
pix_line = pix_line + " ".colorize(:background => find_color(pixel))
end
puts pix_line
end
The method goes through the chain of characters, creating a string of spaces with a colored background for each. It finds each color with a helper method that takes up a lot of vertical space, but starts like this:
def find_color(letter)
case letter
when "R"
:red
when "r"
:light_red
And from there you have an image output to the terminal!