Learning Python and being creative. Making art with code.

Kevin howbrook
7 min readNov 14, 2017

--

What do words look like as colours? What would Shakespear’s sonnets look like as colours? This mini project renders text as colours using Python and saves them in a grid as an image.

All titles of Shakespear’s sonnets

What is this?

It started as a bit of fun late one night on codepen, toying with ways to make images with code. In a really simple way, how can a colour represent a word? Easy… just convert a word to a colour code right?

As it turns out, it wasn’t so easy. Like most developers, I started looking for a JS library that already did this, wishful thinking as it’s not really that useful. I thought this from Charlie Coleman was pretty cool, but I was more interested in showing a more granular representation. A color for each word, no matter how many letters or words.

Why? And Why Python?

Like many developers I have met along the way, my degree wasn’t in a technical subject. In fact, it was Art. More specifically, sculpture and illustration, but, even though my career is in web development, I still have a creative practice. Code really is just another material to work with.

Why Python? Well, JavaScripts was good, but it was slow and getting slower. I heard python was really fast and I planned to be throwing hundreds of words at it.

Planning

After the Codepen version, I came to decide the following:

  • The colours created from the words should get the colour values using every letter, not just the first few.
  • The ‘canvas’ on which the colours sit should be adjustable, I should be able to print a huge image of a whole book, or a small image of my name.
  • The final rendering should be an image, not a web view.

An example: the letters ‘Abcd’ as a word

Given ‘Abcd’ each letter of the word is assigned a value to where it sits in the alphabet like this:

# position   0   1   2   3   4   5  ...
alphabet = ["a","b","c","d","e","f",...]

As RGB values only need 3 numbers, we use the remaining letters to increase the values of the numbers representing the first three letters. So…

  • Word is ‘Abcd’
  • Values are [0,1,2]
  • Remaining values are [3]
  • Add [3] to the first value (0).
Some small word examples

What happens with large words?

This is where it started to get tricky. I want all the letters in a word to affect the 3 RGB color values, leaving them out just seemed dodgy and not fair. So using a zip cycle, we can iterate and add the all values after the third letter accordingly, e.g.:

  • Word is ‘Abcdefghij’
  • Values are [0,1,2]
  • Remaining values are [3,4,5,6,7,8,9]
  • For each of the remaining values, go through the initial values and add them to the 3 RGB values one at a time so,
  • Remaining value 3 is added to val[0]
  • Remaining value 4 is added to val[1]
  • Remaining value 5 is added to val[2]
  • Remaining value 6 is added to val[0]
  • Remaining value 7 is added to val[1]
  • Etc

That zip cycle function saved me a lot of trouble:

for i, j in zip(cycle(range(len(colors))), additions):
colors[i] += j
colors[i] = int(colors[i])

There are also some other nice bits in the code to ensure the values don’t exceed [255,255,255], really it was just something that came up that needed some thought. You’ll notice that in the list above, the initial values could be small, like [0,1,2]. So they are multiplied by (255 /26) or (value limit/letters in alphabet). This gives a better base to add values to. [0,9.8,19.6].

As we start adding the leftover values, they are multiplied, to ensure we are don’t tip over 255, we divide by 26, for values above 3 but lower than 6. Then for words with more than 6 letters, they get really small values by dividing more. (x * n /26)(1/26). Here is that function:

for i,letter in enumerate(letters):
# Only three numbers needed for RGB so use the first three values as a base
# eg: [10, 4, 21]
n = 255.0 / 26.0 # make sure we don't get a value above 255. (z = 26. 25 *10 = 260 but 26 * 9.8 = 248
if i < 3:
# add the first three values to a new list (number)
letterPosition = alphabet.index(letter) * n # *n here to force a good large number base
number.append(letterPosition)
# Use the remaining values to fine tune the first three we have so it will be a better variant
elif i >= 3 and i < 6:
letterPosition = alphabet.index(letter) * n / 26 # /26 here to get a smaller number to add (we don't want (20*10 + 20*10))
number.append(letterPosition)
# For words above six letter, add a further division so we can keep adding values and never reach above 255,
elif i >= 6:
letterPosition = alphabet.index(letter) * n / 26 / 26 # /26/26 to get even smaller numbers probably overkill but it makes sense mathematically.
number.append(letterPosition)
(left) E.E Cummings — I carry your heart with me (right) William Carlos Williams — The Red Wheelbarrow

What about large text inputs?

I made the canvas size a variable, so if large texts are used, you can increase the canvas size… it really will process anything. Here is the King James Bible:

The King James Bible — In squares

Drawing, Plotting, and Some Note-taking.

Having the colours list ready was one part, plotting the squares and drawing was another. This is pretty easy on the web, there are libraries like Masonry that have been able to position things really well. But, I had not even googled a Python image drawing library before. I found one called Pillow that did the trick.

Working out the plotting of the word squares

So why so many notes and working out? Well in the code at this point we have a list (potentially huge) of colour values, e.g.:

[[45,65,221],[36,187,35],[66,84,224]...]

This list needs to be looped over and a square drawn for each item using the values as colours. The hard part here was drawing the squares in a horizontal line and dropping down a row when we hit the end. But how do we know we are at the end? What size should the drop-down value be?

Here is how that’s done with 5 words as an example:

  • Given I know how many squares I have (5) I work out what ‘grid’ I will need. For example, 5 words would require a 3 x 3 grid.
  • Now I know each row and column will consist of three squares, and I have a canvas size of 1000. 1000 / 3 will be the width and height of each inner square. And also the amount of distance I need to shift my drawing points horizontally and vertically.
  • To find what ‘grid’ I need, I created a function (grid(n,m)) that takes the list of words and a list of squared numbers and works that out:
def calculate_m():
# get a list of possible squared numbers, this give us the number of
# rows and columns to use for the canvas, eg 4 = 2 x 2, 6 = 3 x 3...
# Return: [] of squared numbers up to 10000 (hopefully we don't get that high!)
m = []
for i in range(1,10000):
m.append(i*i)
return(m)

def grid(n,m):
# given the amount of words, work out what grid or M we will need
# 5 words should use 9 (m = 9) a 3 x 3 grid
# Parameters:
# n: a list of elements
# m: a list of squared numbers
word_count = len(n)
for i,m in enumerate(m):
if m - word_count > 0 or m - word_count == 0:
# loop stops when the amount of words can fit in a grid
# eg ((m = 9) - (word_count = 5) = greater than 0 so use 9 (3x3)
return float(m)
break

m = grid(convert(), calculate_m())

Finally, all that left is the most confusing iteration I have ever written:

j = 0 # vertical counter
k = 0 # horizontal counter
for i,v in enumerate(convert()):
if i % squares_per_row == 0 and i != 0: # if i is a multiple of squares per row
j += 1 # increments after the squares per row is hit like 0,0,0,1,1,1,2,2,2...
if i % squares_per_row == 0 and i != 0:
k = 0 # increments after the squares per row is hit like 0,1,2,0,1,2...
points = ((k*s,j*s), (k*s, j*s+s),(k*s+s,j*s+s),(k*s+s, j*s)) #set points with incrementing values :/
draw.polygon((points[0], points[1], points[2], points[3]), outline='black', fill = (v[0],v[1],v[2])) # outline='red', fill='blue'
#borders so redraw the same plots with white lines
draw.line((points[0], points[1], points[2], points[3], points[0]), fill="white", width=border_width) # outline='red', fill='blue'
k += 1
im.save('square.jpg') # save the image.

All in all, this started as a bit of fun, but I ended up learning a lot about Python and drawing with code (something I would love to do more of). One really nice feature is that the canvas can be huge and your squares a few millimetres.

--

--