In 2022, I found out what the average programming language color was. Today, I want to transform code itself into pixel art (in less than 100 LOC).
What?
The plan is very simple: I’ll parse a Ruby file to collect all token types, assign each token a color, and then render each one as a pixel in a grid. Sounds like a plan, right?
If you’re only interested in the script, here it goes. Just run ruby
code_picture.rb your-file.rb
, and you’ll get your abstract pixel art. I’ll
be going over some of the most interesting parts of that code. Stick around, and
you might learn something cool.
Parsing Ruby
The whole reason this silly idea exists is me trying out the new Ruby parser: Prism. So, that’s what I’m going to use to parse code. It’s pretty cool that it parses Ruby itself (at least since CRuby 3.3) and also works as a gem.
Anyway, to use this bad boy in a single-file script, I’ll use an inline Gemfile. It will install any dependencies needed when we run the code. I’ll also install Nokogiri because it will be useful soon.
require "bundler/inline"
gemfile do
source "https://rubygems.org"
gem "prism"
gem "nokogiri"
end
After that, using the Prism Ruby API is pretty straightforward:
result = Prism.lex_file(file)
if result.failure?
errors = result.errors.map { " - #{_1.message}" }
abort("Failed to parse file:\n#{errors.join("\n")}")
end
Note that I am only lexing the file (instead of fully parsing it) since we don’t need an Abstract Syntax Tree.
Creating Pixels
If parsing is successful, our tokens will be on result.value
. Now, I want to
create a Pixel
object out of them. First of all, let’s create a data object
that holds that information.
Using Ruby 3.2’s Data class makes defining Value objects a walk in the park:
Pixel = Data.define(:color, :token_type, :token_value)
So, each Pixel
has a color, the token type (e.g. KEYWORD_DEF
,
PARENTHESIS_LEFT
), and its value (i.e., the variable name, the string
contents, etc). While we’re at it, let’s create a function that returns random
colors and a THEME (a mapping of token types to colors):
# nicer colors than just pure random
def rand_color
"hsl(#{rand(0..360)},#{rand(70..110)}%,#{rand(40..90)}%)"
end
# This ensures random colors while also ensuring the same
# color is used for the same token type.
random_colors = {}
RANDOM_COLORS_THEME = ->(token_type) {
random_colors[token_type] ||= rand_color
}
Cool! Now let’s create the pixel objects. I’ll also filter out some undesired
tokens (things like EOF and new lines) in the way. Our friend filter_map
is here to help with that:
# Prism returns a token and its location in an array.
# I'm using argument destructuring to ignore the second
# value in the array
pixels = result.value.filter_map do |(token, _)|
next if token.type == :WORDS_SEP
next if token.type == :IGNORED_NEWLINE
next if token.type == :NEWLINE
next if token.type == :EOF
next if token.type == :MISSING
next if token.type == :NOT_PROVIDED
Pixel.new(THEME[token.type], token.type, token.value)
end
Now, we’re ready to render HTML.
Rendering HTML
For the fun of keeping it all in Ruby, I decided to use Nokogiri to generate HTML. To keep all the pixel rows about the same size, I used our old friend Math.
row_size = Math.sqrt(pixels.size).ceil
Who would’ve thought square roots were actually useful?
I also used CSS’s attr to pass data from HTML to CSS. The template looks something like this:
builder = Nokogiri::HTML4::Builder.new do |doc|
doc.html do
doc.style do
# Some CSS here
end
doc.body do
pixels.each_slice(row_size).each do |row|
doc.div(class: "row") do
row.each do |pixel|
doc.span(
class: "pixel",
style: "background-color: #{pixel.color}",
"data-content": "Type: #{pixel.type}; Value: #{pixel.value.inspect}"
)
end
end
end
end
end
end
Then, it writes the HTML to a file and opens it with the (MacOS-specific)
open
command:
File.write("code-picture.html", builder.to_html)
`open code-picture.html`
And… done! Here’s what the code_picture.rb
script itself looks like:
Hopefully, it’s not underwhelming! 😅
A Challenge For You!
If you like this, I have a polished version of this script released as a gem. It has several customization options so you can define your own themes, control the pixel size, the number of pixels per row, and more! Give it a try!
Now, I want to make a challenge for you. Can you write valid Ruby code that also renders as cool pixel art? Feel free to use the customization options above to achieve your goal! If you create something cool, please share it on Twitter/X or Mastodon using the hashtag #codepicture (you can also tag me @MatheusRich)! I’d love to see what you come up with!