generated from mwc/project_drawing
346 lines
13 KiB
Python
346 lines
13 KiB
Python
#from superturtle by Chris Proctor
|
|
|
|
from turtle import *
|
|
from itertools import chain, cycle
|
|
|
|
from pathlib import Path
|
|
from shutil import rmtree
|
|
from subprocess import run
|
|
from turtle import (
|
|
Turtle,
|
|
left,
|
|
right,
|
|
clear,
|
|
home,
|
|
heading,
|
|
setheading,
|
|
isdown,
|
|
hideturtle,
|
|
penup,
|
|
pendown,
|
|
forward,
|
|
getcanvas,
|
|
)
|
|
|
|
from itertools import cycle
|
|
from time import time, sleep
|
|
|
|
FRAMES_PATH = Path(".frames")
|
|
|
|
class no_delay:
|
|
"""A context manager which causes drawing code to run instantly.
|
|
|
|
For example::
|
|
|
|
from turtle import forward, right
|
|
from superturtle.movement import fly, no_delay
|
|
fly(-150, 150)
|
|
with no_delay():
|
|
for i in range(720):
|
|
forward(300)
|
|
right(71)
|
|
input()
|
|
"""
|
|
|
|
def __enter__(self):
|
|
self.n = tracer()
|
|
self.delay = delay()
|
|
tracer(0, 0)
|
|
|
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
update()
|
|
tracer(self.n, self.delay)
|
|
|
|
|
|
def update_position(x, y=None):
|
|
"""
|
|
Updates the turtle's position, adding x to the turtle's current x and y to the
|
|
turtle's current y.
|
|
Generally, this function should be called with two arguments, but it may
|
|
also be called with a list containing x and y values::
|
|
|
|
from superturtle.movement import update_position
|
|
update_position(10, 20)
|
|
update_position([10, 20])
|
|
"""
|
|
if y is None:
|
|
x, y = x
|
|
current_x, current_y = position()
|
|
penup()
|
|
goto(x + current_x, y + current_y)
|
|
pendown()
|
|
|
|
|
|
class restore_state_when_finished:
|
|
"""
|
|
A context manager which records the turtle's position and heading
|
|
at the beginning and restores them at the end of the code block.
|
|
For example::
|
|
|
|
from turtle import forward, right
|
|
from superturtle.movement import restore_state_when_finished
|
|
for angle in range(0, 360, 15):
|
|
with restore_state_when_finished():
|
|
right(angle)
|
|
forward(100)
|
|
"""
|
|
|
|
def __enter__(self):
|
|
self.position = position()
|
|
self.heading = heading()
|
|
|
|
def __exit__(self, *args):
|
|
penup()
|
|
setposition(self.position)
|
|
setheading(self.heading)
|
|
pendown()
|
|
|
|
|
|
def animate(frames=1, loop=False, debug=False, gif_filename=None):
|
|
"""Runs an animation, frame by frame, at a fixed frame rate of 20 fps.
|
|
An animation consists of a bunch of frames shown one after another.
|
|
Before creating an animation, create a static image and then think about
|
|
how you would like for it to move. The simplest way to use `animate` is
|
|
with no arguments; this produces a static image. (Not much of an animation!)::
|
|
|
|
for frame in animate():
|
|
draw_my_picture(frame)
|
|
|
|
Once you are happy with your static image, specify `frames` and `animate`
|
|
will run the provided code block over and over, drawing one frame at a time::
|
|
|
|
for frame in animate(frames=6, debug=True):
|
|
draw_my_picture(frame)
|
|
|
|
Because we set `debug` to `True`, you will see the following output in the Terminal.
|
|
Additionally, since we are in debug mode, you need to press enter to advance one
|
|
frame at a time.::
|
|
|
|
Drawing frame 0
|
|
Drawing frame 1
|
|
Drawing frame 2
|
|
Drawing frame 3
|
|
Drawing frame 4
|
|
Drawing frame 5
|
|
|
|
Arguments:
|
|
frames (int): The total number of frames in your animation.
|
|
loop (bool): When True, the animation will play in a loop.
|
|
debug (bool): When True, renders the animation in debug mode.
|
|
gif_filename (str): When provided, saves the animation as a gif, with 10 frames per second.
|
|
"""
|
|
start_time = time()
|
|
if frames <= 0:
|
|
raise AnimationError("frames must be a positive integer")
|
|
frame_delta = 0.05
|
|
hideturtle()
|
|
if debug:
|
|
frame_iterator = debug_iter(frames)
|
|
elif loop and not gif_filename:
|
|
frame_iterator = cycle(range(frames))
|
|
else:
|
|
frame_iterator = range(frames)
|
|
if gif_filename:
|
|
if FRAMES_PATH.exists():
|
|
if FRAMES_PATH.is_dir():
|
|
rmtree(FRAMES_PATH)
|
|
else:
|
|
FRAMES_PATH.unlink()
|
|
FRAMES_PATH.mkdir()
|
|
|
|
for frame_number in frame_iterator:
|
|
frame = Frame(frames, frame_number, debug=debug)
|
|
if time() < start_time + frame_number * frame_delta:
|
|
sleep(start_time + frame_number * frame_delta - time())
|
|
with no_delay():
|
|
home()
|
|
clear()
|
|
yield frame
|
|
if gif_filename:
|
|
filename = FRAMES_PATH / f"frame_{frame_number:03d}.eps"
|
|
canvas = getcanvas()
|
|
canvas.postscript(file=filename)
|
|
canvas.delete("all")
|
|
|
|
if gif_filename:
|
|
loopflag = "-loop 0 " if loop else ""
|
|
run(f"convert -delay 5 -dispose Background {loopflag}{FRAMES_PATH}/frame_*.eps {gif_filename}", shell=True, check=True)
|
|
rmtree(FRAMES_PATH)
|
|
|
|
|
|
class Frame:
|
|
""" Represents one frame in an animation.
|
|
When creating an animation, `animate` will yield one `Frame` for each
|
|
frame. The `Frame` can be used to check information, such as the current
|
|
frame index (the first frame's index is 0; the tenth frame's index is 9).
|
|
The `Frame` can also be used to create motion through the use of transformations.
|
|
A transformation is a change to the picture which gradually changes from frame
|
|
to frame. The three supported transformations are `rotate`, `scale`, and
|
|
`translate`.
|
|
"""
|
|
def __init__(self, num_frames, index=0, debug=False):
|
|
self.debug = debug
|
|
self.num_frames = num_frames
|
|
self.index = index
|
|
self.stack = []
|
|
self.log("Drawing frame {}".format(self.index))
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
exit = self.stack.pop()
|
|
exit()
|
|
|
|
def rotate(self, start, stop=None, first_frame=None, last_frame=None, cycles=1, mirror=False, easing=None):
|
|
"""Runs the code block within a rotation::
|
|
|
|
for frame in animate(frames=30):
|
|
with frame.rotate(0, 90):
|
|
square(100)
|
|
|
|
Arguments:
|
|
start (int): the initial value.
|
|
stop (int): (optional) the final value. If not provided, this will be a static
|
|
rotation of `start` on every frame.
|
|
first_frame (int): (optional) The first frame at which this rotation should be
|
|
interpolated. If given, the rotation will be `start` at `first_frame`
|
|
and all prior frames. If not given, interpolation starts at the beginning
|
|
of the animation.
|
|
last_frame(int): (optional) The last frame at which this rotation should be
|
|
interpolated. If given, the rotation will be `stop` at `last_frame`
|
|
and all later frames. If not given, interpolation ends at the end of the
|
|
animation.
|
|
cycles (int): (optional) Number of times the animation should be run.
|
|
mirror (bool): (optional) When True, the animation runs forward and then backwards
|
|
between `first_frame` and `last_frame`.
|
|
easing (function): (optional) An easing function to use.
|
|
"""
|
|
value = self.interpolate(start, stop, first_frame, last_frame, cycles, mirror, easing)
|
|
left(value)
|
|
self.stack.append(lambda: right(value))
|
|
self.log("Rotating by {}".format(value))
|
|
return self
|
|
|
|
|
|
def scale(self, start, stop=None, first_frame=None, last_frame=None, cycles=1, mirror=False, easing=None):
|
|
"""Scales the code block. For this to work correctly, make sure you start from the
|
|
center of the drawing in the code block::
|
|
|
|
for frame in animate(frames=30):
|
|
with frame.scale(1, 2):
|
|
square(100)
|
|
|
|
Arguments:
|
|
start (int): the initial value.
|
|
stop (int): (optional) the final value. If not provided, this will be a static
|
|
scaling of `start` on every frame.
|
|
first_frame (int): (optional) The first frame at which this scaling should be
|
|
interpolated. If given, the scaling will be `start` at `first_frame`
|
|
and all prior frames. If not given, interpolation starts at the beginning
|
|
of the animation.
|
|
last_frame (int): (optional) The last frame at which this scaling should be
|
|
interpolated. If given, the scaling will be `stop` at `last_frame`
|
|
and all later frames. If not given, interpolation ends at the end of the
|
|
animation.
|
|
cycles (int): (optional) Number of times the animation should be run.
|
|
mirror (bool): (optional) When True, the animation runs forward and then backwards
|
|
between `first_frame` and `last_frame`.
|
|
easing (function): (optional) An easing function to use.
|
|
"""
|
|
value = self.interpolate(start, stop, first_frame, last_frame, cycles, mirror, easing)
|
|
repair = self._scale_turtle_go(value)
|
|
self.stack.append(repair)
|
|
self.log("Scaling by {}".format(value))
|
|
return self
|
|
|
|
|
|
def translate(self, start, stop=None, first_frame=None, last_frame=None, cycles=1, mirror=False, easing=None):
|
|
"""Translates (moves) the code block in the current coordinate space::
|
|
|
|
for frame in animate(frames=30):
|
|
with frame.translate([0, 0], [100, 100]):
|
|
square(100)
|
|
|
|
Arguments:
|
|
start (int): the initial value.
|
|
stop (int): (optional) the final value. If not provided, this will be a static
|
|
translation of `start` on every frame.
|
|
first_frame (int): (optional) The first frame at which this translation should be
|
|
interpolated. If given, the translation will be `start` at `first_frame`
|
|
and all prior frames. If not given, interpolation starts at the beginning
|
|
of the animation.
|
|
last_frame (int): (optional) The last frame at which this translation should be
|
|
interpolated. If given, the translation will be `stop` at `last_frame`
|
|
and all later frames. If not given, interpolation ends at the end of the
|
|
animation.
|
|
cycles (int): (optional) Number of times the animation should be run.
|
|
mirror (bool): (optional) When True, the animation runs forward and then backwards
|
|
between `first_frame` and `last_frame`.
|
|
easing (function): (optional) An easing function to use.
|
|
"""
|
|
if stop:
|
|
x0, y0 = start
|
|
x1, y1 = stop
|
|
dx = self.interpolate(x0, x1, first_frame, last_frame, cycles, mirror, easing)
|
|
dy = self.interpolate(y0, y1, first_frame, last_frame, cycles, mirror, easing)
|
|
else:
|
|
dx, dy = start
|
|
|
|
def scoot(x, y):
|
|
pd = isdown()
|
|
penup()
|
|
forward(x)
|
|
left(90)
|
|
forward(y)
|
|
right(90)
|
|
if pd:
|
|
pendown()
|
|
|
|
scoot(dx, dy)
|
|
self.stack.append(lambda: scoot(-dx, -dy))
|
|
|
|
self.log("Translating by ({}, {})".format(dx, dy))
|
|
return self
|
|
|
|
|
|
def _scale_turtle_go(self, scale_factor):
|
|
"""Patches `Turtle._go` with a version which scales all motion
|
|
by `scale_factor`. Returns a repair function which will restore
|
|
`Turtle._go` when called.
|
|
"""
|
|
prior_go = Turtle._go
|
|
def scaled_go(turtle_self, distance):
|
|
prior_go(turtle_self, distance * scale_factor)
|
|
Turtle._go = scaled_go
|
|
def repair():
|
|
Turtle._go = prior_go
|
|
return repair
|
|
|
|
def log(self, message):
|
|
if self.debug:
|
|
print(" " * len(self.stack) + message)
|
|
|
|
|
|
def debug_iter(max_val=None):
|
|
"An iterator which yields only when input is "
|
|
HELP = '?'
|
|
INCREMENT = ['', 'f']
|
|
DECREMENT = ['b']
|
|
value = 0
|
|
print("In debug mode. Enter {} for help.".format(HELP))
|
|
while True:
|
|
yield value % max_val if max_val else value
|
|
command = None
|
|
while command not in INCREMENT and command not in DECREMENT:
|
|
if command == HELP:
|
|
print("Debug mode moves one frame at a time.")
|
|
print("Enter 'f' or '' (blank) to move forward. Enter 'b' to move backward.")
|
|
command = input()
|
|
if command in INCREMENT:
|
|
value += 1
|
|
else:
|
|
value -= 1
|
|
|
|
class AnimationError(Exception):
|
|
pass |