"""
turtleBeads.py
v0.3
Turtle graphics library for drawing various shapes centered on the
cursor.
In general, these functions draw things centered at the cursor, and put
the cursor back where it started afterwards. Set the pensize and pencolor
before drawing a shape to control what is drawn. For most shapes you can
also use fillcolor and begin_fill/end_fill to fill in the shape.
"""
import math
import random
from turtle import *
# Setup function
#---------------
def setupTurtle():
"""
Sets up the turtle window using default size, speed, pen size, and
pen/fill colors.
"""
try:
setup()
except:
pass
setup()
reset()
speed(0)
pensize(1.5)
color("black", "black")
# TODO: Move turtle window to front
# Trace control
#--------------
def noTrace():
"""
Disables turtle tracing, so that drawing will be near-instant (much
faster than even speed 0). However, nothing will be displayed until
you call showPicture.
"""
tracer(0, 0)
def doTrace():
"""
Re-enables tracing, so that the turtle will move along the path that
it draws and you can see each line being drawn. This function first
updates the picture to display any lines drawn since tracing was
disabled (if it had been).
"""
update()
# TODO: What args here?
tracer(1, 1)
def showPicture():
"""
Shows any lines drawn so far. Required when noTrace has been called
to disable real-time drawing.
"""
update()
# Movement shortcuts
#-------------------
def realign():
"""
Sets the turtle's heading back to the default (0 degrees = facing
right).
"""
setheading(0)
def teleport(x, y):
"""
Penup + goto + pendown.
"""
downNow = isdown()
penup()
goto(x, y)
if downNow:
pendown()
def leap(dist):
"""
Penup + fd + pendown. You can use a negative number to go backwards.
"""
downNow = isdown()
penup()
fd(dist)
if downNow:
pendown()
def hop(dist):
"""
Lifts the pen and moves the given distance to the left of the current
turtle position without changing the orientation of the turtle (hops
sideways). Use a negative number to hop to the right. Puts the pen
back down when it's done if the pen was down beforehand.
"""
downNow = isdown()
penup()
lt(90)
fd(dist)
rt(90)
if downNow:
pendown()
# Drawing parameters
#-------------------
BASE_CURVE_STEPS = 32 # Default number of sides of a circle
MAX_CURVE_STEPS = 128 # Maximum number of sides for a circle
TARGET_SEGMENT_LENGTH = 3 # Ideal length for each side of a circle
# "Beads" functions
#------------------
def drawCircle(radius):
"""
Draws a circle centered at the given position with the given radius,
and puts the turtle back where it started when it's done.
Actually, it draws a many-sided polygon, but the difference should
usually be hard to see.
"""
downNow = isdown()
steps = BASE_CURVE_STEPS
segmentLength = (2*math.pi*radius) / steps
while segmentLength > TARGET_SEGMENT_LENGTH and steps < MAX_CURVE_STEPS:
steps += 1
segmentLength = (2*math.pi*radius) / steps
start = pos()
starth = heading()
penup()
lt(90)
fd(radius)
rt(90)
if downNow:
pendown()
fd(segmentLength/2)
rt(360/steps)
for i in range(steps-1):
fd(segmentLength)
rt(360/steps)
penup()
fd(segmentLength/2)
goto(start[0], start[1])
seth(starth)
if downNow:
pendown()
def ellipsePointAt(major, minor, angle):
"""
Takes an angle in degrees and computes the ellipse point for that many
degrees clockwise from the top of the ellipse where the given minor
radius is vertical and the given major radius is horizontal. Uses the
trammel drawing method from:
https://www.joshuanava.biz/engineering-3/methods-of-drawing-an-ellipse.html
The angle specified is interpreted as the trammel angle, not an angle
of a ray from the center of the ellipse through the given point.
"""
rad = math.radians(90 - angle)
yIntercept = -(major - minor) * math.sin(rad)
xValue = major * math.cos(rad)
yValue = yIntercept + major * math.sin(rad)
return (xValue, yValue)
def drawEllipse(radius, aspectRatio, arcAngle=None):
"""
Draws an ellipse with the given radius and aspect ratio. If aspectRatio
is less than 1, the given radius will be the ellipse's larger radius,
and the ellipse will stretch farther to the sides of the turtle than
in front of and behind it, otherwise the given radius will be the
smaller radius, and the ellipse will stretch farther to the front and
back than to the sides (the given radius is always the distance from
the turtle's current position to the sides of the ellipse directly
left and right of the turtle).
There is an optional argument 'arcAngle,' which will cause this
function to draw only part of an ellipse. The ellipse segment is
drawn starting at the left of the current cursor position if the
aspect ratio is greater than or equal to 1, or starting behind the
current cursor position if the aspect ration is less than 1.
"""
# Measure starting position/orientation
downNow = isdown()
startPos = pos()
startHeading = heading()
headingAdjust = 0
# Decide minor/major axes and start angle based on aspect ratio:
if aspectRatio >= 1:
minor = radius
major = radius*aspectRatio
startAngle = 0
# Get into position to start the ellipse:
penup()
lt(90)
fd(minor)
rt(90)
if downNow:
pendown()
here = (0, minor)
else:
minor = radius*aspectRatio
major = radius
startAngle = -90
headingAdjust = -90
# Get into position to start the ellipse:
penup()
lt(90)
fd(major)
rt(90)
if downNow:
pendown()
here = (-major, 0)
# Compute number of segments to draw based on estimated segment length:
steps = BASE_CURVE_STEPS
segmentLength = (2*math.pi*major) / steps
while segmentLength > TARGET_SEGMENT_LENGTH and steps < MAX_CURVE_STEPS:
steps += 1
segmentLength = (2*math.pi*major) / steps
# Actually draw the ellipse:
stop = False
for i in range(1, steps+1):
nextAngle = startAngle + i*360/steps
if arcAngle != None and nextAngle > startAngle + arcAngle:
stop = True
there = ellipsePointAt(major, minor, startAngle + arcAngle)
else:
there = ellipsePointAt(major, minor, nextAngle)
vec = ( there[0] - here[0], there[1] - here[1])
# Compute heading in unrotated ellipse and distance to travel:
towardsNext = math.degrees(math.atan2(vec[1], vec[0]))
dist = (vec[0]*vec[0] + vec[1]*vec[1])**0.5
# Draw segment:
setheading(startHeading + headingAdjust + towardsNext)
fd(dist)
# Update here -> there
here = there
if stop:
break
# Return to original position and heading:
penup()
goto(startPos[0], startPos[1])
setheading(startHeading)
if downNow:
pendown()
def drawDot(radius):
"""
Draws a circle filled with the current pen color of the given radius.
Does not move the turtle. For large circles, this may be more round
than the result of the drawCircle function, and it will also be
faster, but the limitation is that the circle will always be filled
in, and the pen color will be used as the fill color (can't have
separate border + fill colors).
"""
oldSize = pensize()
pensize(radius)
fd(0)
pensize(oldSize)
def drawSquare(size):
"""
Draws a square of the given size centered on the current turtle
position. Puts the turtle back when it's done.
"""
drawRectangle(size, size);
def drawRectangle(length, width):
"""
Draws a rectangle of the given length (in front of and behind the turtle)
and width (to the left and right of the turtle) centered on the current
turtle position. Puts the turtle back when it's done.
"""
downNow = isdown()
penup()
lt(90)
fd(width/2)
rt(90)
bk(length/2)
if downNow:
pendown()
fd(length)
rt(90)
fd(width)
rt(90)
fd(length)
rt(90)
fd(width)
rt(90)
penup()
fd(length/2)
rt(90)
fd(width/2)
lt(90)
if downNow:
pendown()
def drawPolygon(sideLength, numSides):
"""
Draws a polygon with the given side length and number of sides,
centered at the current position. numSides must be at least 3, or
nothing will be drawn. The polygon created is always equilateral, and
always has one side perpendicular to the current heading that's to the
left of the current turtle position (left based on the current turtle
heading).
"""
downNow = isdown()
if numSides < 3:
return
else:
# (sideLength/2) / center-side distance = tan(theta/2)
# so center-side distance = (sideLength/2) / tan(theta/2)
sideAngle = 360/numSides
centerSideDist = (sideLength/2) / math.tan(math.radians(sideAngle)/2)
penup()
lt(90)
fd(centerSideDist)
rt(90)
if downNow:
pendown()
fd(sideLength/2)
rt(sideAngle)
for i in range(numSides-1):
fd(sideLength)
rt(sideAngle)
fd(sideLength/2)
penup()
rt(90)
fd(centerSideDist)
lt(90)
if downNow:
pendown()
# Text drawing
#-------------
FONT_SIZE = 18
TEXT_ALIGN = "center"
def fontsize(size):
"""
Sets the current font size. The default font size is 18. The argument
must be a number, and will be rounded to the nearest integer.
"""
global FONT_SIZE
FONT_SIZE = int(abs(size))
def align(where):
"""
Sets the current text alignment. The default is "center". The
argument must be one of the strings "center", "left", or "right",
or there will be no effect.
"""
global TEXT_ALIGN
if where in ("center", "left", "right"):
TEXT_ALIGN = where
def drawText(text):
"""
Draws the given text using the current font size and alignment (see
the fontsize and align functions). The text is drawn due North of the
current turtle position, no matter what direction the turtle is
facing, and cannot be rotated. Either the left edge, the center, or
the right edge of the text will be directly above the turtle,
depending on the current alignment setting. The turtle is not moved
by this command.
If the text contains a newline character, multiple lines of text will
be written.
"""
write(text, False, TEXT_ALIGN, ("Arial", FONT_SIZE, "normal"))
# Misc. Helpers
#--------------
def randomPastelColor():
"""
Returns a random pastel color.
"""
return random.choice([
"LightSkyBlue",
"LightPink",
"PaleGreen",
"PaleGoldenrod",
])
def randomVibrantColor():
"""
Returns a random well-saturated color.
"""
return random.choice([
"Blue",
"Navy",
"Red",
"DarkRed",
"Green",
"ForestGreen",
"Yellow",
"Purple",
"SaddleBrown",
"SeaGreen",
"Orange",
"VioletRed",
])
def randomMutedColor():
"""
Returns a random faded color.
"""
return random.choice([
"Aquamarine3",
"DarkSeaGreen3",
"DarkOrange3",
"GoldenRod3",
"DarkSlateGray4",
"IndianRed3",
"Salmon3",
"MediumPurple2",
"Plum3",
"OliveDrab3",
"PaleGreen3",
])
# Testing
#--------
def test():
"""
Tests this module by drawing various shapes in a grid.
"""
setupTurtle()
teleport(-200, 200)
drawCircle(50)
print("Circle done...")
speed(7)
teleport(-100, 200)
drawEllipse(50, 0.5)
print("Ellipse 1 done...")
teleport(0, 200)
drawEllipse(40, 1.5)
print("Ellipse 2 done...")
teleport(100, 200)
drawDot(50)
print("Filled circle done...")
teleport(200, 200)
drawSquare(50)
print("Square done...")
teleport(-200, 100)
drawRectangle(50, 75)
print("Rectangle 1 done...")
teleport(-100, 100)
drawRectangle(75, 50)
print("Rectangle 2 done...")
teleport(0, 100)
drawPolygon(40, 3)
print("Polygon 1 done...")
teleport(100, 100)
drawPolygon(40, 5)
print("Polygon 2 done...")
teleport(200, 100)
drawPolygon(20, 12)
print("Polygon 3 done...")
teleport(0, 0)
drawText("Hello\nWorld")
print("Text done...")
if __name__ == "__main__":
test()