Draw the Limacon of Pascal with Turtle

Viewed 65

I'd like to replicate this picture using Turtle.

enter image description here

I apparently have problems with spatial thinking, so the code just loops, although according to my idea it should scroll.

from turtle import*
from math import* 
speed(10)
down()
fd(200);bk(400);
goto(0,0);
left(90);
fd(200);bk(400);
goto(0,0);
stamp();
right(180);
a=50;
l=25;
circle(a);
goto(0,0);
l=25
for t in range(0,360):
 t=t+0.1
 r=a*cos(pi)+l
 circle(r,t)
mainloop()

I understand that the problem is in the loop, but I don't know how to overcome it.

2 Answers

First of all, please do yourself (and others reading your code) a favor and use whitespace between all operators and arguents. Never abuse ; to smush multiple expressions onto one line. Use Black to format your code if in doubt (you can use it online). Remember that you're writing code for humans first, computers second. It's fine to compress for code golf, but not when you're building an app, asking for help or writing an assignment.

Secondly, I suggest using the long form of all Python turtle commands. fd -> forward and so on, for similar reasons as above. The abbreviated versions are mostly there for historical reasons (and for those who struggle with or are new to typing).

Avoid from foo import *. It pollutes the namespace and makes code less readable because you can't tell which functions belong to you or a module, and if they're from a module, which one. Prefer importing the module as a whole so that all functions are called on the module name. I know from turtle import * is typical in turtle, but I'd suggest import turtle and create an instance as your project grows. If you do use from syntax, avoid the *; unpack each item explicitly.

Indent blocks 4 spaces in Python.


As for the logic, the code doesn't seem to make much sense or have a particular overall strategy that I can intuit.

For example,

for t in range(0, 360):
    t = t + 0.1

probably isn't what you expect; the t sequence is 0.1, 1.1, 2.1.... So 0.1 is an offset.

The code is drawing 360 small circles over and over again in pretty much the same place. The goal here is to draw one circle, with the special condition that the angle changes gradually during the course of drawing it. The special condition makes a builtin like circle useless, because it can't draw anything but a perfect circle.

I'm sure there is a clever mathematical formula you can look up that will plot this perfectly, but since I don't know much math, I've hacked it by following the gif on the Limaçon Wikipedia and used the one trig formula I'm familiar with at present.

The idea (from my mathematically untrained eye) is to first plot a circle, which can be done with the classic x = cos(a) * r, y = sin(a) * r pattern, or by moving the turtle gradually forward while turning.

The next step is to draw another circle that "rolls" along the outside of the inner circle. The same formula for drawing the larger circle can be applied once again to draw the smaller circle with its own angle.

The below approach is ugly and hardcoded, but I'm pretty sure it can be teased into mathematical precision as an exercise; the important point is that there's a strategy behind the code that gets things in the right ballpark.

from turtle import exitonclick, goto, pendown, penup, tracer
from math import cos, pi, sin

r = 100
rr = 90
aa = 0
tracer(0)
penup()

for a in range(379):
    a = a * pi / 180
    x = cos(a) * r
    y = sin(a) * r
    xx = cos(aa) * rr + x
    yy = sin(aa) * rr + y
    goto(xx, yy)
    pendown()
    aa += 0.035

exitonclick()

Feel free to clean this up if anyone can generalize the math.

The thing that catches my eye is this equation in your loop:

r=a*cos(pi)+l

it makes the radius a constant, but it should vary during the loop. This is my approach built upon a unit circle:

from turtle import Screen, Turtle
from math import pi, sin, cos

screen = Screen()
screen.setup(400, 400)
screen.setworldcoordinates(-2, -2, 2, 2)

turtle = Turtle()
turtle.hideturtle()
turtle.radians()

for _ in range(2):
    turtle.forward(2)
    turtle.backward(4)
    turtle.forward(2)
    turtle.left(pi/2)

turtle.penup()
turtle.color('blue')

theta = 0.02
angle = 0

while angle <= 2 * pi:
    radius = 0.5 + cos(angle)

    x = cos(angle) * radius
    y = sin(angle) * radius

    turtle.setposition(x, y)
    turtle.pendown()

    angle += theta

screen.exitonclick()

enter image description here

We can expand this to make the rolling circle that @ggorlen describes explicit (i.e. animated), using a cardioid this time:

from turtle import Screen, Turtle
from math import pi, sin, cos

RADIUS = 1.0

screen = Screen()
screen.setup(400, 400)
screen.setworldcoordinates(-4, -4, 4, 4)
screen.tracer(False)

indelible = Turtle()
indelible.hideturtle()
indelible.radians()

for _ in range(2):
    indelible.forward(4)
    indelible.backward(8)
    indelible.forward(4)
    indelible.left(pi/2)

indelible.penup()
indelible.width(3)
indelible.color('blue')
indelible.sety(RADIUS)
indelible.pendown()
indelible.circle(RADIUS, steps=30)
indelible.penup()
indelible.color('red')

erasable = Turtle()
erasable.hideturtle()
erasable.radians()
erasable.width(3)
erasable.penup()

theta = 0.02
angle = 0

while angle <= 2 * pi:
    x = cos(angle) * RADIUS
    y = sin(angle) * RADIUS

    erasable.setposition(x, y)
    erasable.setheading(angle)
    erasable.pendown()
    erasable.circle(RADIUS, steps=30)
    erasable.penup()

    indelible.setposition(erasable.position())
    indelible.setheading(erasable.heading())
    indelible.circle(RADIUS, extent=angle)
    indelible.dot()

    erasable.setposition(indelible.position())
    erasable.dot()

    screen.update()
    erasable.clear()
    angle += theta

screen.exitonclick()

enter image description here

Note that in both examples I switch the turtle into radians to avoid conversions between degrees, which turtle uses by default, and radians which the math library functions use.

Related