Color Picker

Intro

This tutorial was created as a small introduction for beginner type-designers, who just started to code in Python. The main idea behind these articles is meant to show the design process of creating the tools in Python. On simple example, I will try to explain the logic behind the code. I will show my way of doing. I’m assuming, that you already know the basics of Python syntax.

For me, coding really helps in solving the problems. Before I start coding, I usually come across one. If the most efficient way of solving the problem is creating the code, then I’m starting to work on the tool that could help me. Before I’m starting to write the actual code, I’m thinking how the final result of my programming should look like: should it have User Interface? If yes, then how it should interact? Should it display something? If yes, then what is the best way of displaying it? Should it be widely available or will it be useful just for me? etc. This is the crucial part of the job. The more you describe your project at this stage, the easier coding is.

Color Pallet

Before I started to work on one of my tools, Master Check Tool, I was looking for a nice way of browsing through the colors. I needed to choose the set of RGB values for displaying the contour shapes across the master. I wanted them to be pretty.

I came up with the idea to use free DrawBot app, great application, that allows you to create vector 2D graphics with Python code. I wanted to create a simple code, that would display the color’s RGB values, HASH values and color itself as a set of the colored circles displayed on one page. This way I would be able to compare different colors and choose those, that I consider pretty.

The DrawBot application has its own set of methods and objects which allows you to draw shapes in the DrawBot's drawing window. I will describe those of them, that I’m going to use in the article. To run the code inside DrawBot, you need to write it in the code window. But first, let’s focus on the code actual code.

In order to create a color pallet, I needed a way of representing a single color sample.

I thought that It would be useful not only to display the color, but its also RGB and HASH values. So If I will pick the color, I will be able to copy those values to any kind of code that I will work on in the future.

Single Color Sample

Since the RoboFont and DrawBot use RGB values, where every value is described by the decimal number between 0 and 1, I don’t display those values as values between 0 and 255.

The first thing I had to do was to find a way to display the circle like this. In order to do that I needed to tell DrawBot to display text, circle, and text's and circle's color.

If you are already familiar with DrawBot’s methods, please skip next section.

DrawBot’s basics

Ovals and Texts

DrawBot’s function that draws circles and ellipsis is called oval. Oval takes four parameters: origin point’s x-value (x), origin point’s y-value (y), oval’s width (w), oval’s height (h).

oval( x, y, w, h )

The origin point is the same point as the bottom left corner of the oval’s bounding box. It points where the shape is going to be drawn. The oval’s width and height values determine how big this oval is gonna be.

Try to use this code in the DrawBot. Write it into the code window and press Run button or press CMD + R.

x = 200
y = 300
    w = 400
h = 320
oval( x, y, w, h )

Since the default fill color in the DrawBot is black, you will see black ellipsis, stretched horizontally (because width’s value (400) is greater than height value (320)).

First Oval

Bounding Box

The concept of the origin point is important. It repeats in the many DrawBot’s functions and objects. Such as rect, which basically takes the same parameters as oval function. It draws a rectangle. Function text that we are going to use to display RGB and HASH values on the pallet also take origin point as one of the main parameters. Beside those coordinates, it takes a string value, which contains characters that are going to be printed in the drawing window.

text(txt, (x,y))

Colors and Order calling functions in DrawBot

You can set the properties of the elements that are going to be drawn, such as fill color, stroke color, stroke weight, font size, font family. To do that you have to write down the settings by using special DrawBot’s functions. Attention! The order is important. For example: if you will first set the stroke’s width and color and then set the text, the letters will have an outline, which usually is not wanted.

Let’s say, that I want to display a circle with orange outline and text next to it. At first, intuition tells me to first set the color property for the stroke, then stroke’s width. Then I would draw an oval. Then I would print out the text.

# the method that sets the color values (color values in DrawBot are represented by decimal numbers in between 0 and 1)
stroke(1,.3,0)
# default value of the strokeWidth is 0. If you don't set this, you won't see any stroke at all
strokeWidth(5)

oval(200,200,300,300)

# for the educational purposes I'm showing here the fontSize function, which ( guess what !?! ) sets the size of the font rendered in DrawBot
fontSize(100)
text("DAMN!!!",(550,300))

In the code above I’ve just done that. Let’s run this and see the results:

Outlined Text

Unfortunately, we set the stroke’s properties before the text command. That’s why not only the oval was drawn with the stroke but also the letters.

There are three different solutions for this problem:

  1. Move the text function along with the fontSize (fonfSize should be above the text function if we want it to have an effect on displayed letters) above the settings of the stroke, like that:
fontSize(100)
text("Better!",(550,300))

stroke(1,.3,0)
strokeWidth(5)
oval(200,200,300,300)

The only problem of that solution is if I would like to print the text on top of the circle, it won’t work. Let’s move the text a little bit to the left (by changing the x coordinate of the text’s origin point), so the text and circle would overlay each other. And while we are here, let’s change the fill color of the oval to red. This way we will see if the text is under or above the circle. We do it with the fill function, which takes RGB parameters in the same way as the stroke function.

fontSize(100)
text("Better!",(420,300)) # I've changed coordinates from 550 to 420, so now text overlays the circle

fill(1,0,0) # it changes the fill of any drawn object in the DrawBot, including the text
stroke(1,.3,0)
strokeWidth(5)
oval(200,200,300,300)

  1. Resetting the color properties for the different elements of the image.

In this solution we have the same order as in the beginning:

In the beginning at first, we set the stroke parameters for the oval. Then we draw the circle. Below the oval function we set the fontSize. And then we gave the command to draw a text.

I. stroke

II. strokeWidth

III. oval

IV. fontSize

V. text

The difference is that after the oval and before the text command I will put the stroke description, that will reset the stroke properties for text and everything that would be drawn after it. Let’s also change the fill color of the oval like in the solution No 2.

fill(1,0,0) # red fill for the oval
stroke(1,.3,0)
strokeWidth(5)
oval(200,200,300,300)

stroke(None) # if stroke() or fill() gets None as a only parameter, DrawBot doesn't draw the particular thing
fontSize(100)
text("Quite Ok!",(430,300))

If you will run this code in DrawBot, you will see this: Reset Stroke’s Color As you can see, the text has the same fill color as the circle in the background. You can see that the letter ”Q” is above the circle’s stroke. One thing achieved!!!

Now, the easy part is to change the fill for the text just by writing second fill command in between the oval and text functions. Let’s make the text color black.

fill(1,0,0)
stroke(1,.3,0)
strokeWidth(5)
oval(200,200,300,300)

stroke(None)
fill(0,0,0) # setting the fill color to Black. It will effect text color
fontSize(100)
text("Quite Ok!",(430,300))

Now everything looks ok!!! Let’s analyse what just happened: In the code above first we changed the color of the fill from default black to red. Then we changed the thickness and color of the stroke from default to orange. After we did it, we started to change every of those property to look the same as in default configuration. The same effect we can achieve with much less code.

  1. We’ve could achieve the same results by using the pair of special Drawbot functions: save and restore. Those functions don’t take any arguments. The relationship between them is significant as well as order of the usage. How they work? If you’ve changed drawing settings such as fill or stroke and you want to go back to the settings that were in use, before you’ve changed anything you use save and restore.

Let’s write this code:

save()
fill(1,0,0)
stroke(1,.3,0)
strokeWidth(5)
oval(200,200,300,300)

restore()
fontSize(100)
text("Magnificent!",(380,300))

Save saves the settings at the point, where it has been called. To restore those saved settings, you use function restore. It is really simple. This way, we hand’t to write down fill and stroke definitions for the text.

The result is analogical to previous method:

Save() Restore() usage

Canvas operations

This is probably the hardest part of the DrawBot to understand. Which is not hard at all :) . If you run any code that draws something i the DrawBot, you realise that it uses coordinate system, with two axes: X and Y. Where the axis x is horizontal and the axis y is vertical. The beginning of the coordinate system (also known as an origin point, or point with coordinates (0,0) ) as a default is in the bottom left corner of the drawing window.

Let’s introduce the term commonly used in the web development: Canvas. In my words: Canvas is a space where the drawings appear, that is stored inside the drawing window. You can move the canvas without moving the drawing window. How? let’s see that!

Let’s draw small red oval in the beginning of the coordinate system:

sizeOfTheOval = 10
radius = sizeOfTheOval/2
fill(1,0,0)
oval(-radius, -radius, sizeOfTheOval, sizeOfTheOval)

Lets also draw the part of the axis that vale is grater than zero. We will display it as a perpendicular lines, touching in the beginning of the coordinate system.

In DrawBot to draw the line all you need is two sets of coordinates expressed as a couple with two numbers (integers or decimal numbers) and function line. In our example, in both lines the first coordinates will be equal to (0,0).

sizeOfTheOval = 10
radius = sizeOfTheOval/2
fill(1,0,0)
oval(-radius, -radius, sizeOfTheOval, sizeOfTheOval)

# setting up the stroke colors:
strokeWidth(1)
stroke(0.5,0.2,0.5)
# axis y
line((0,0), (0,10000))

# axis x
line((0,0), (10000,0))

If you will run this, you will merely see the difference. But lets move the canvas. I will move it 100 units up and 200 units left. The name of the command that we use to move the canvas is translate. It takes two arguments: X value (horizontal movement) and Y value (vertical movement). Run this code:

translate(200, 100) # I moved the canvas 100 units up and 200 units left
sizeOfTheOval = 10
radius = sizeOfTheOval/2
fill(1,0,0)
oval(-radius, -radius, sizeOfTheOval, sizeOfTheOval)

strokeWidth(1)
stroke(0.5,0.2,0.5)
line((0,0), (0,10000))
line((0,0), (10000,0))

Moving canvas is like moving the sheet with the dishes on it: it will move everything that is on it (that has been written in the code below moving command). If you will put some drawing command above the translate it won’t affect it.

Now let’s rotate this canvas by 10 degrees clockwise.

translate(200, 100)
rotate(-10) # canvas rotates clockwise, when passed values in degree units is less than zero. Counter clockwise, when the value is grater than zero.
sizeOfTheOval = 10
radius = sizeOfTheOval/2
fill(1,0,0)
oval(-radius, -radius, sizeOfTheOval, sizeOfTheOval)

strokeWidth(1)
stroke(0.5,0.2,0.5)
line((0,0), (0,10000))
line((0,0), (10000,0))

You can also use a scale() function to scale the canvas. If you want to scale down for example to 30% of ordinal scale, you pass 0.3 decimal number: scale(0.3). If you want to have a twice big canvas, you pass 2 (scale(2)), etc.

Color circle

So, lets continue on creating the pallet. First let’s create a color sample. In order to do that we will need a circle. Lets draw a circle at the beginning of the coordinate system:

s = 100
oval(0, 0, s, s)

Just for the sake of the nice code, let’s close it in the function. Let’s call this function drawColor:

def drawColor():
    s = 100
    oval(0, 0, s, s)

I imagine, that one of the most important info about this circle is its color. I would like this function to have only one parameter. The RGB value, the HASH value and the fill color will be based on this parameter. Let’s define this parameter and without waiting, pass it to the fill.

def drawColor(colorValues):
    s = 100
    fill(colorValues)
    oval(0, 0, s, s)

This code is not completed. How this parameter should look like? What should it be? In my opinion the most handy way of passing the color values is through the iterable tuple or list. If we would call this function with tuple that contains three decimal numbers (e.g. drawColor((0.5, 0.2, 0.1)) ) it wouldn’t work. Why? Because fill function needs three different parameters, and we passed only one. We can either unwrap this, by passing unwrapped tuple to the fill function ( fill(*coloValues) - asterix before the tuple or list unwraps its elements), either unwrap this, by assigning tuple to three different variables. I will choose the latter option. Just to make sure, that nothing goes wrong let’s raise assert error if the tuple contains different number of elements than three.

def drawColor(colorValues):
    assert len(colorValues) == 3, “colorValues should contain three values”

    # unwrapping by assigning tuple to three different variables:
    r, g, b = colorValues

    s = 100
    fill(r, g, b)
    oval(0, 0, s, s)

Now lets apply text description. Close lines with fill and oval function inside of save()-restore(), like this:


    s = 100
    save()
    fill(r, g, b)
    oval(0, 0, s, s)
    restore()

Below unwrapped colorValues, define two variables hashValue and rgbValue. Those will be the strings passed to text functions. I’m using formatted strings for this peruse.

def drawColor(colorValues):
    assert len(colorValues) == 3, "colorValues should contain three values"

    r, g, b = colorValues
    rgbValue = '%r, %r, %r' % (r, g, b)
    hashValue = '#%02x%02x%02x' % (int(r*255), int(g*255), int(b*255))

    s = 100
    radius = s/2
    save()
    fill(r, g, b)
    oval(0, 0, s, s)
    restore()

Now it’s time to add text function. After the save()-restore() block lets add those three lines.

    font('Monaco',s/10)
    lineHeight(s/5)
    text("%s\n#%s" % (rgbValue, hashValue), (radius, radius+1), align='center')

First two lines are font settings: font('Monaco', s/10) – this is the basic font setting. The first argument that font function takes is the name of the typeface that is going to be use. The second argument is font size that is going to be used. I’m going to use Monaco, since this typeface is a mono spaced. That is going to be more useful during displaying text on the circle (the width of the text is more predictable). This function is combination of other two functions. I could replace it by

fontFamily('Monaco')
fontSize(s/10)

lineHeight(s/5) - changes the text leading value

So now my code looks like this:

def drawColor(colorValues):
    assert len(colorValues) == 3, "colorValues should contain three values"

    r, g, b = colorValues
    rgbValue = '%r,%r,%r' % (r, g, b)
    hashValue = '%02x%02x%02x' % (int(r*255), int(g*255), int(b*255))

    radius = s/2
    save()
    fill(r, g, b)
    oval(0, 0, s, s)
    restore()

    font('Monaco',s/10)
    lineHeight(s/5)
    text("%s\n#%s" % (rgbValue, hashValue), (radius, radius+1), align='center')

s = 100 # now this function is global
size(s,s)
color = (round(random(),2),round(random(),2),round(random(),2))
drawColor( color )

I added random color generator, which generates tuple, that contains three decimal numbers. I called function drawColor with color variable as a parameter. I also moved s variable outside the function. So it is not local function anymore. With this variable I called size(s,s), which changes the size of the drawing window (from default 1000x1000 units).

Creating the set of colors

In order to display the color pallet we need a set of color values. We are going to generate those with the for loops.

First let me explain why the for loop. Probably you used the code similar to this:

numberOfSteps = 10
for step in range(numberOfSteps):
    print(step)

It iterates through every number starting from 0 to 9. During every iteration it prints every step.

We can use this formula to get decimal number, that we could pass to our drawColor function. To do that we need to divide the step inside the for loop by numberOfSteps. This way we will get 10 decimal numbers starting from 0 and ending on 0.9. For now lets do it without calling drawColor:

numberOfSteps = 10
for step in range(numberOfSteps):
    decimalNumber = step/numberOfSteps
    print(decimalNumber)

If you want to use drawColot, you can see that there is only one decimalNumber to use. Don’t worry about it for now, I will explain the problem after showing the next code. We are gonna use newPage and saveImage functions. Those are DrawBot function: first one creates new canvas. The second one allows to save all created canvas as an animation or images.

def drawColor(colorValues):
    assert len(colorValues) == 3, "colorValues should contain three values"

    r, g, b = colorValues
    rgbValue = '%r,%r,%r' % (r, g, b)
    hashValue = '%02x%02x%02x' % (int(r*255), int(g*255), int(b*255))

    radius = s/2
    save()
    fill(r, g, b)
    oval(0, 0, s, s)
    restore()

    font('Monaco',s/10)
    lineHeight(s/5)
    text("%s\n#%s" % (rgbValue, hashValue), (radius, radius+1), align='center')

s = 100 # in this snippet I won't use size() function, since every iteration creates a newPage

numberOfSteps = 50
for step in range(numberOfSteps):
    colorValue = step/numberOfSteps
    color = ( colorValue, colorValue, colorValue)
    newPage(s,s)
    drawColor( color )

saveImage("color-animation-01.gif")

So as you can see, we used the same float for every color value. It resulted in animated grayscale in 50 steps. And everyone can notice that the last value is not full RGB white (for full white every value has to be equal to 1). Solving the latter problem is easy. In the line with for loop command add 1 to the numberOfSteps:

numberOfSteps = 50
for step in range(numberOfSteps+1):
    colorValue = step/numberOfSteps
    color = ( colorValue, colorValue, colorValue)
    newPage(s,s)
    drawColor( color )

saveImage("color-animation-01.gif")

The first problem is a little bit more complicated. The way you want to see the colors depends on your idea for the pallet. You could pass random float for every iteration to the color variable, or you could make the variation of the color values only for red, and the rest would have a fixed values.

What I wanted when I designed my pallet was to see every combination of the color values within given boundaries, which now are expressed as numberOfSteps.

Let go back to the first code with for loop:

numberOfSteps = 10
for step in range(numberOfSteps):
    print(step)

What you would do to get every combination of two floats between 0 and 9? I would put one for loop inside the other. Both of them will iterate through 10 steps.

numberOfSteps = 10
for stepA in range(numberOfSteps):
    for stepB in range(numberOfSteps):
        print(stepA, stepB)

It will result in 100 combinations. Nice, huh? But we need to have combinations of three values. So let’s just put the third for loop inside of the second one.

numberOfSteps = 10
for stepA in range(numberOfSteps):
    for stepB in range(numberOfSteps):
        for stepC in range(numberOfSteps):
            print(stepA, stepB, stepC)

Lets change it into the format, in which we will be able to define the colors:

numberOfSteps = 10
for stepA in range(numberOfSteps):
    for stepB in range(numberOfSteps):
        for stepC in range(numberOfSteps):
                r, g, b = (stepA/numberOfSteps, stepB/numberOfSteps, stepC/numberOfSteps)
                print(r,g,b)

And to be sure, that the white color is fully white, lets add 1 to every numberOfSteps in the for loop declaration.

numberOfSteps = 10
for stepA in range(numberOfSteps+1):
    for stepB in range(numberOfSteps+1):
        for stepC in range(numberOfSteps+1):
                r, g, b = (stepA/numberOfSteps, stepB/numberOfSteps, stepC/numberOfSteps)
                print(r,g,b)

Everything combined:

def drawColor(colorValues):
    assert len(colorValues) == 3, "colorValues should contain three values"

    r, g, b = colorValues
    rgbValue = '%r,%r,%r' % (r, g, b)
    hashValue = '%02x%02x%02x' % (int(r*255), int(g*255), int(b*255))

    radius = s/2
    save()
    fill(r, g, b)
    oval(0, 0, s, s)
    restore()

    font('Monaco',s/10)
    lineHeight(s/5)
    text("%s\n#%s" % (rgbValue, hashValue), (radius, radius+1), align='center')

s = 100

numberOfSteps = 10
for stepA in range(numberOfSteps+1):
    for stepB in range(numberOfSteps+1):
        for stepC in range(numberOfSteps+1):
            r, g, b = (stepA/numberOfSteps, stepB/numberOfSteps, stepC/numberOfSteps)
            color = ( r, g, b )
            newPage( s, s )
            drawColor( color )

saveImage("color-animation-02.gif")

Well, it is nice, that we have this colourful animated gif. But for me it is useless. It is hard to compare the color samples with each other. I would prefer to have multiple colors on the single page. To be able to see the different options at the same time. In order to do that I will have to collect different combinations if the color values and store them in the list. I will call that list colorCollection.

### From now on, I will omit the **drawColor** definition, but of course it should be there

s = 40
numberOfSteps = 10
colorCollection = []
for stepA in range(numberOfSteps+1):
    for stepB in range(numberOfSteps+1):
        for stepC in range(numberOfSteps+1):
            r, g, b = (stepA/numberOfSteps, stepB/numberOfSteps, stepC/numberOfSteps)
            color = ( r, g, b )
            colorCollection.append( color )
            newPage( s, s )
            drawColor( color )


saveImage("color-collection-01.png")

For now, this code still creates the same animation. To change that I will delete newPage(). I will replace it with size outside the for loops. I will move drawColor to a new for loop. But this time the new for loop won’t iterate through a range of numbers, but through colors stored in colorCollection.

s = 40
numberOfSteps = 10
colorCollection = []
for stepA in range(numberOfSteps+1):
    for stepB in range(numberOfSteps+1):
        for stepC in range(numberOfSteps+1):
            r, g, b = (stepA/numberOfSteps, stepB/numberOfSteps, stepC/numberOfSteps)
            color = ( r, g, b )
            colorCollection.append( color )
size( s, s ) # size again gets "s" as a only arguments

for color in colorCollection:
    drawColor( color )

saveImage("color-collection-01.png")     

After running this code we can see only the RGB and HASH values indicating that the color that we see is white. What just happened is every sample of the color inside the colorCollection has been drawn on top of the rest.

To display every color, first I will change the size declaration from size(s, s) to size(w, h), where w = s * len(colorCollection) and h = s. This way I will create canvas, that will have enough space to display every color sample in one line. But in order to place every color sample next to each other, I will need to move the origin point during every iteration by s value to the right.

s = 40
numberOfSteps = 10
colorCollection = []
for stepA in range(numberOfSteps+1):
    for stepB in range(numberOfSteps+1):
        for stepC in range(numberOfSteps+1):
            r, g, b = (stepA/numberOfSteps, stepB/numberOfSteps, stepC/numberOfSteps)
            color = ( r, g, b )
            colorCollection.append( color )

w = s * len(colorCollection)
h = s
size( w, h )

for color in colorCollection:
    drawColor( color )
    translate(s, 0) # next sample is going to be drawn after the one just drawn

saveImage("color-collection-01.png")     

That is much better!!! But still, one continuous line of the colors is hard to browse. In order to change this, we have to introduce a few things:

  1. Starting drawing from top left corner.
  2. line breaks

Starting drawing from top left corner is quite easy. Why I want it to be drawn that way? Since I was raised on latin script, it is easier for me to read the data this way: starting from top, read towards right till the end of the line.

The problem is that by default canvas origin point is on the left bottom corner of the drawing window. We can change it. Imagine that we want to draw the circle. The size of the circle is 50. If we want it to be drawn in bottom left corner, the circle’s origin point has to be the same as beginning of coordinate system. But if we want it to be drawn in top left corner, we have to either change the origin point of the circle or translate (move) the canvas’ origin point.

For this project, translate will be more useful. We want only move the canvas along Y axis, so first argument for the translate will be equal to 0. The second one should be equal to the height of the page minus the size of the circle. To get the height value without doing any unnecessary calculations I will use BrawBot’s height() function, which return this value (there is also the width() function).

size = 75
translate(0, height() - size)
oval(0, 0, size, size)

Knowing that I’m able to apply this to my code. Since it is starting point for of drawing lines of the color palette, I have to put it outside the for loop, where the color sample are going to be drawn.

### From now on, I will omit the whole block where I'm collecting the color values to the "collorCollection" list. I also moved "s" variable down.

s = 40
w = s * len(colorCollection)
h = s
size( w, h )

# chaning the starting point to top left corner of the page
translate(0, height() - s)

for color in colorCollection:
    drawColor( color )
    translate(s, 0)

saveImage("color-collection-02.png")     

For the final image, this doesn’t make any difference. But it will when we will introduce the line breaks.

Let’s go back to the circle example. Let say that we have similar loop to the one in our color pallet code, but the size is not defined by us:

sizeOfTheCircle = 75
numberOfSteps = 100
translate(0, height() - size)

for step in range(numberOfSteps):
    oval( 0, 0, sizeOfTheCircle, sizeOfTheCircle )
    translate(sizeOfTheCircle, 0)

It draws 200 circles, but most of them are outside the canvas. It is possible to describe after how many circles, the line should be broken. To determine brake point we will use modulus (%) operator and a new variable, which describes how many elements are gonna be in one line.

sizeOfTheCircle = 75
numberOfSteps = 100
elementsInLine = 8

translate(0, height() - sizeOfTheCircle)
for step in range(numberOfSteps):
    oval( 0, 0, sizeOfTheCircle, sizeOfTheCircle )
    translate(sizeOfTheCircle, 0)

    step += 1
    if step % elementsInLine == 0 :
        translate(-elementsInLine * sizeOfTheCircle, -sizeOfTheCircle)

Two last lines are crucial. If the reminder from the division of the step value by elementsInLine equals 0 then the origin point move down by sizeOfTheCircle value and shifts to the left of the drawing window (-elementsInLine * size).

During every iteration I’m increasing the step value by 1. Otherwise we would have problem with division by 0 during the first iteration.

Fitting the size of the canvas is really easy. Especially if it comes to the width. Because width is the size of the single circle multiplied by number of the elements in one line (elementsInLine value). Determining the height is a little bit harder, but still quite easy. In order to know what exactly will be the second argument for size function, I have to define the number of lines. It should be the division of numberOfSteps by elementsInLine. But if the last line contains less elements than the value of elementsInLine variable, then we are ending up with the decimal number. And it would just sound silly. You never say, that column of the text has 10.7 lines. We have to round this number up. To do that you will have to import ceil function from the math module. It rounds floats up.

After rounding the division, we can multiply the result by the sizeOfTheCircle value. The result can be passed as the second argument to size function.

from math import ceil


sizeOfTheCircle = 75
numberOfSteps = 75
elementsInLine = 8
numberOfLines = ceil(numberOfSteps/elementsInLine)

w = elementsInLine * sizeOfTheCircle
h = sizeOfTheCircle * numberOfLines

size(w, h)

translate(0, height() - sizeOfTheCircle)
for step in range(numberOfSteps):
    oval( 0, 0, sizeOfTheCircle, sizeOfTheCircle )
    translate(sizeOfTheCircle, 0)

    step += 1
    if step % elementsInLine == 0:
        translate(-elementsInLine * sizeOfTheCircle, -sizeOfTheCircle)

Combining it with our pallet is not that difficult. I will focus on it now:

  1. In the beginning of the script I’m putting the import of the ceil function from the math module.
  2. I’m creating two new variables:
  3. a. elementsInLine
  4. b. numberOfLines: in the previous example, the numberOfLines depended on division of the numberOfsteps by elementsInLine. Now the situation is going to be a little bit different. Since the for loop that draws the circles is not related to numberOfsteps. Instead of it it is based on the colorCollection list. So I’m exchanging the numberOfSteps with lenght of the colorCollection list len(colorCollection)
  5. I’m changing the width and height values (w and h), so the drawing will perfectly fit the canvas
from math import ceil # 1

def drawColor(colorValues):
    assert len(colorValues) == 3, "colorValues should contain three values"

    r, g, b = colorValues
    rgbValue = '%r,%r,%r' % (r, g, b)
    hashValue = '%02x%02x%02x' % (int(r*255), int(g*255), int(b*255))

    radius = s/2
    save()
    fill(r, g, b)
    oval(0, 0, s, s)
    restore()

    font('Monaco',s/10)
    lineHeight(s/5)
    text("%s\n#%s" % (rgbValue, hashValue), (radius, radius+1), align='center')


numberOfSteps = 20
colorCollection = []
for stepA in range(numberOfSteps+1):
    for stepB in range(numberOfSteps+1):
        for stepC in range(numberOfSteps+1):
            r, g, b = (stepA/numberOfSteps, stepB/numberOfSteps, stepC/numberOfSteps)
            color = ( r, g, b )
            colorCollection.append( color )

s = 10

elementsInLine = 21 # 2 - a
numberOfLines = ceil(len(colorCollection)/elementsInLine) # 2 - b

w = elementsInLine * s # 3
h = s * numberOfLines # 3

size(w, h)

translate(0, height() - s)
for step, color in enumerate(colorCollection):
    drawColor( color )
    translate(s, 0)

    step += 1
    if step % elementsInLine == 0:
        translate(-elementsInLine * s, -s)