Remember the good old days of programming in LOGO? I do, at least! Back then it was the most 1337 thing to do. I remember waiting one week for that one day when I could go to my computer tuition and could spend 1 hour trying to get my name to draw. If you have forgotten how to write a LOGO program, here’s a quick one –
FD 100 RT 90 FD 100
“FD” moves the turtle forward by the given value (and draws a line), “RT” rotates the turtle by the angle given (in degrees). The commands to go backward, and rotate left are”BK” and “LT” respectively. There are other commands, but we won’t deal with them now.
Yesterday, It suddenly dawned on me, can I write a LOGO emulator in Python? My goal wasn’t getting it as accurate as possible, but to efficiently implement the moving and rotating part.
The end result is pretty decent. The program is not interactive. You write the commands in a text file, and pass it to the program, and it generates an SVG with the picture. For example, passing this
FD 100 RT 90 FD 100 RT 90 FD 100 RT 90 FD 100
Produces this –
Also it only recognizes only FD, BK, RT, and LT and ignores anything else. Also no error checking for now, maybe later!
The important part was not writing the code, but implementing the movements, and there is a lot of Maths involved.
I could have implemented it using normal coordinate and line equations, but since I wanted efficiency, I decided to use complex numbers. If you have forgotten complex numbers, here’s a quick reminder –
Complex numbers are basically supercharged coordinates. They are just normal pairs of real numbers , but what makes them different from normal coordinates are that they can be added and multiplied (in Maths terms, they form a field)
The addition and multiplication are given by –
Just as an aside, to make this complete, the first coordinate is called the real part and the second coordinate is called the imaginary part. A complex number with imaginary part 0 (i.e. numbers of the form (a, 0)) is called a purely real number, and numbers with real part zero (numbers of the form (0, b)) is called a purely imaginary number.
Moreover, we identify the number (a, 0) as a. What we have done is that we have effectively extended the real numbers by adding a 0 as their second coordinate, and made them complex numbers.
We denote (0, 1) as i and note that a point (a, b) can be written as –
So, a point can be represented as
The value of is the distance of the point from origin, known as modulus of , and the angle the point makes with the positive x axis is knows as the amplitude or argument of the complex number. This turns out to be but it’s a little complicated and we won’t go into much detail. Check out the following GeoGebra applet.
Why complex numbers?
Now comes the real question? Why are complex numbers efficient to implement movements? The (simplified) reason is that they can be interpreted as vectors too!
The point can also act as the position vector of the same point which, in familiar setting, would mean the vector where are the basis vectors.
Moving from one point by a certain distance in a certain direction is as easy as multiplying the unit vector by the distance and add to the current point.
For example, if I wanted to move from (2,3) in the upward direction by 5 units, I have to first get the unit vector along the upward direction. If you have forgotten what a unit vector is, it’s a vector (complex number) with length (modulus) 1. In this case it would be the vector (0,1). So, we multiply (0,1) by 5, and add to (2,3) and get (2,8) and this will be our destination.
Rotating is also easy. Rotating a vector by an angle is as easy as multiplying it by which is the same as . If is positive, it will be a counter clockwise rotation otherwise a clockwise rotation.
Check out the following applet. Try moving the unit vector OC around and see how the angle of rotation changes.
So now you see, instead of keeping track of the direction and position separately we can embed in into one vector, and we don’t even have to deal with any line equations or other complicated things, just simple complex number addition and multiplication is all, and good news? Python supports them by default!
Is that all?
Nope, there’s one more difficulty. The
svgwrite module, which we will use, uses a standard coordinate system for image processing, where the origin is situated at the top left corner and the y-axis is flipped. It means, y increases as we go down.
Now, we will use a different system. Firstly, our drawing will start from the center. Which means if we use a 1000 by 1000 image, (500, 500) will be our origin (0, 0). So we have to translate the origin. Hence, the point (a, b) in our system will become (a + 500, b + 500) in the actual system.
Also, in our system, y will increase as we go up, so to tackle that, we’ll internally reverse the directions. So, forwarding by 100 units in our system will be reverting by 100 units in the actual system and a left turn in our system would be a right turn in the actual system.
Off to coding
Finally, we are off to coding, phew! We start with installing the
svgwrite module –
pip install svgwrite
Then create a new file. I’m calling it
First, we start by doing the necessary imports –
import svgwrite import sys from math import sin, cos, radians
Then we create two vectors. One would be the current position vector and another would be the current direction vector. We will start pointing upwards. Hence our initial direction would be (0,1)
direction_vector = complex(0,1) current_pos = complex(0,0)
Now we write our parser code. It will take two parameters – the input filename, and the output filename. It will –
- Open the input file
- Prepare the SVG.
- Read the input file line by line
- Split each line into two parts – command and value
- Handle the commands
- Save the SVG
def parse(inp, out): with open(inp) as f: lines =f.readlines() dwg = svgwrite.Drawing(out, (1000, 1000)) dwg.add(dwg.rect((0,0), (1000,1000), fill=svgwrite.rgb(255,255,255))) for line in lines: comm, val = line.split(" ") val = int(val) prev_pos = current_pos if comm == "FD": go_to_point(-val) elif comm == "BK": go_to_point(val) elif comm =="LT": rotate(radians(val)) elif comm == "RT": rotate(-radians(val)) else: print("Unknown command %s, ignoring"%comm) dwg.add(dwg.line((prev_pos.real + 500, 500 + prev_pos.imag), (current_pos.real + 500, 500 + current_pos.imag), stroke=svgwrite.rgb(255, 0, 0))) dwg.save()
The code is pretty self-explanatory. We have created a document of 1000 by 1000 size, and filled it with a white rectangle. We have also saved our current position in a variable called
prev_pos so that we can draw the line after we move.
While handling FD and BK, we have used a function called
go_to_point which we’ll write soon. Remember the y-axis flip? This is why we are passing
-val in FD and
val in BK. Similar case is with the RT and LT.
And finally, we are drawing a line from the previous to current point, accounting for the origin translation.
Now we write the
go_to_point function. This one is easy, we multiply the direction vector by the distance and add with the current point, and update the current position.
def go_to_point(distance): global direction_vector, current_pos current_pos += direction_vector * distance print("Moving to: ", current_pos)
And the rotate function is similar too. We take the direction vector and multiply it by the appropriate vector –
def rotate(angle): global direction_vector direction_vector *= complex(round(cos(angle),5), -round(sin(angle),5)) print("Rotating to ", direction_vector)
You might have noticed we have rounded off to 5 decimal places. The reason is that floating point arithmetic is never exact due to how floating point numbers are stored. This might cause trouble with trigonometric functions –
>>> cos(radians(90) 6.123233995736766e-17
This problem will cause our turtle to go off axis if we rotate through 90 degrees, although by a little bit. So we round off to 5 digits, which is enough for our purpose.
Finally, we take the filenames, and parse –
inp = sys.argv out = sys.argv parse(inp, out)
And that’s it. Let’s try a few inputs –
Create a file called
input.txt and type this in –
FD 100 RT 90 FD 100 RT 90 FD 100 RT 90 FD 100
And run –
python input.txt output.svg
Then open the resulting SVG with an image viewer. The output for this one is given above.
Try this one –
FD 300 RT 127 FD 500 LT 127 FD 300
So that ends our “Poor man’s LOGO” for now. Later we will add more functions to it. And here’s the whole file –
import svgwrite import sys from math import sin, cos, radians direction_vector = complex(0,1) current_pos = complex(0,0) def parse(inp, out): with open(inp) as f: lines =f.readlines() dwg = svgwrite.Drawing(out, (1000, 1000)) dwg.add(dwg.rect((0,0), (1000,1000), fill=svgwrite.rgb(255,255,255))) for line in lines: comm, val = line.split(" ") val = int(val) prev_pos = current_pos if comm == "FD": go_to_point(-val) elif comm == "BK": go_to_point(+val) elif comm =="LT": rotate(radians(val)) elif comm == "RT": rotate(-radians(val)) else: print("Unknown command %s, ignoring"%comm) dwg.add(dwg.line((prev_pos.real + 500, 500 + prev_pos.imag), (current_pos.real + 500, 500 + current_pos.imag), stroke=svgwrite.rgb(255, 0, 0))) dwg.save() def rotate(angle): global direction_vector direction_vector *= complex(round(cos(angle),5), -round(sin(angle),5)) print("Rotating to ", direction_vector) def go_to_point(distance): global direction_vector, current_pos current_pos += direction_vector * distance print("Moving to: ", current_pos) inp = sys.argv out = sys.argv parse(inp, out)