diff --git a/__pycache__/shapes.cpython-310.pyc b/__pycache__/shapes.cpython-310.pyc index e6cce42..50ed9fb 100644 Binary files a/__pycache__/shapes.cpython-310.pyc and b/__pycache__/shapes.cpython-310.pyc differ diff --git a/__pycache__/superturtledraw.cpython-310.pyc b/__pycache__/superturtledraw.cpython-310.pyc new file mode 100644 index 0000000..49ef846 Binary files /dev/null and b/__pycache__/superturtledraw.cpython-310.pyc differ diff --git a/drawing.py b/drawing.py index df7d816..7179f90 100644 --- a/drawing.py +++ b/drawing.py @@ -6,14 +6,48 @@ from turtle import * from shapes import * -from superturt import * +from superturtledraw import * fly(0,-150) -colors = ['green', 'greenyellow', 'yellow', 'goldenrod', 'darkorange', 'orange', 'orangered', 'red', 'violetred4', 'violetred', 'purple4', 'midnightblue'] -for color in colors: +colormode(255) +rcolor = 0 +gcolor = 225 +bcolor = 0 +for bcolor in range(0,225,100): with no_delay(): with restore_state_when_finished(): draw_tree_nl(25) with restore_state_when_finished(): - draw_tree_wl(25, color) \ No newline at end of file + draw_tree_wl(25, rcolor, gcolor, bcolor) +for gcolor in range(225,0,-100): + with no_delay(): + with restore_state_when_finished(): + draw_tree_nl(25) + with restore_state_when_finished(): + draw_tree_wl(25, rcolor, gcolor, bcolor) +for rcolor in range(0,225,100): + with no_delay(): + with restore_state_when_finished(): + draw_tree_nl(25) + with restore_state_when_finished(): + draw_tree_wl(25, rcolor, gcolor, bcolor) + +for bcolor in range(225,0,-100): + with no_delay(): + with restore_state_when_finished(): + draw_tree_nl(25) + with restore_state_when_finished(): + draw_tree_wl(25, rcolor, gcolor, bcolor) +for gcolor in range(0,225,100): + with no_delay(): + with restore_state_when_finished(): + draw_tree_nl(25) + with restore_state_when_finished(): + draw_tree_wl(25, rcolor, gcolor, bcolor) +for rcolor in range(225,0,-100): + with no_delay(): + with restore_state_when_finished(): + draw_tree_nl(25) + with restore_state_when_finished(): + draw_tree_wl(25, rcolor, gcolor, bcolor) \ No newline at end of file diff --git a/shapes.py b/shapes.py index 567783d..f16a45e 100644 --- a/shapes.py +++ b/shapes.py @@ -1,10 +1,10 @@ from turtle import * from math import sqrt -from superturt import * +from superturtledraw import * -def draw_leaf(size, color): +def draw_leaf(size, rcolor, gcolor, bcolor): pencolor('black') - fillcolor(color) + fillcolor((rcolor, gcolor, bcolor)) begin_fill() circle(size,90) right(270) @@ -12,10 +12,10 @@ def draw_leaf(size, color): circle(-.25*size,90) end_fill() -def draw_leaves(color): +def draw_leaves(rcolor, gcolor, bcolor): with restore_state_when_finished(): for number in range(11): - draw_leaf(20, color) + draw_leaf(20, rcolor, gcolor, bcolor) penup() forward(10) right(115) @@ -27,12 +27,12 @@ def draw_branch_nl(size): branch_end_nl(size) circle(size/8,angle) -def draw_branch_wl(size, color): +def draw_branch_wl(size, rcolor, gcolor, bcolor): angles = [165, 150, 150, 150, 150, 150, 150, 165, 105] for angle in angles: - branch_end_wl(size, color) + branch_end_wl(size, rcolor, gcolor, bcolor) circle(size/8,angle) - draw_leaves(color) + draw_leaves(rcolor, gcolor, bcolor) def draw_turn(d1,a1,d2,a2,d3,a3,d4): forward(d1) @@ -46,30 +46,30 @@ def draw_turn(d1,a1,d2,a2,d3,a3,d4): def tip_nl(size): draw_turn(size,45,size/10,90,size/10,45,size) -def tip_wl(size, color): +def tip_wl(size, rcolor, gcolor, bcolor): tip_nl(size) - draw_leaves(color) + draw_leaves(rcolor, gcolor, bcolor) def branching_nl(ang1, d1, ang2, d2, ang3): draw_turn(0,ang1,d1,ang2,d2,ang3,0) -def branching_wl(ang1, d1, ang2, d2, ang3, color): +def branching_wl(ang1, d1, ang2, d2, ang3, rcolor, gcolor, bcolor): branching_nl(ang1, d1, ang2, d2, ang3) - draw_leaves(color) + draw_leaves(rcolor, gcolor, bcolor) -def branch_end_wl(size, color): +def branch_end_wl(size, rcolor, gcolor, bcolor): for number in range(3): forward(size) - draw_leaves(color) + draw_leaves(rcolor, gcolor, bcolor) forward(size) right(300) - tip_wl(size, color) - branching_wl(240,size,330,size,330, color) - tip_wl(size, color) - branching_wl(30,size,240,size,30, color) - tip_wl(size, color) - branching_wl(330,size,330,size,240, color) - tip_wl(size, color) + tip_wl(size, rcolor, gcolor, bcolor) + branching_wl(240,size,330,size,330, rcolor, gcolor, bcolor) + tip_wl(size, rcolor, gcolor, bcolor) + branching_wl(30,size,240,size,30, rcolor, gcolor, bcolor) + tip_wl(size, rcolor, gcolor, bcolor) + branching_wl(330,size,330,size,240, rcolor, gcolor, bcolor) + tip_wl(size, rcolor, gcolor, bcolor) right(300) forward(4*size) @@ -97,11 +97,11 @@ def draw_tree_nl(size): circle(size/4,90) end_fill() -def draw_tree_wl(size, color): +def draw_tree_wl(size, rcolor, gcolor, bcolor): penup() circle(size/4,90) forward(10*size) circle(size/8,105) - draw_branch_wl(size, color) + draw_branch_wl(size, rcolor, gcolor, bcolor) forward(10*size) circle(size/4,90) \ No newline at end of file diff --git a/superturt.py b/superturt.py deleted file mode 100644 index ea3f8d5..0000000 --- a/superturt.py +++ /dev/null @@ -1,390 +0,0 @@ -#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 - -from turtle import getcanvas -from pathlib import Path -from subprocess import run - -def save(filename): - """Saves the canvas as an image. - - Arguments: - filename (str): Location to save the file, including file extension. - """ - temp_file = Path("_temp.eps") - getcanvas().postscript(file=temp_file) - cmd = f"convert {temp_file} -colorspace RGB {filename}" - run(cmd, shell=True, check=True) - temp_file.unlink() - -from turtle import * -from itertools import chain, cycle - -def fly(x,y): - """Moves the turtle to (x,y) without drawing. - - This is really just a simple shortcut for lifting the pen, - going to a position, and putting the pen down again. But it's such - a commonly-used pattern that it makes sense to put it into a function. - Here's an example:: - - from turtle import forward, right - from superturtle.movement import fly - - def square(size): - for side in range(4): - forward(size) - right(90) - - for col in range(10): - for row in range(10): - fly(40 * col, 40 * row) - square(20) - """ - penup() - goto(x,y) - pendown() \ No newline at end of file diff --git a/superturtledraw.py b/superturtledraw.py new file mode 100644 index 0000000..2e1efa5 --- /dev/null +++ b/superturtledraw.py @@ -0,0 +1,831 @@ +__version__ = '0.0.2' + +# lines.py +# by Chris Proctor +# Helper functions for playing with how the turtle draws + +# ============================================================================= +# ! Advanced ! +# ============================================================================= +# This module contains some fancy code that we don't expect you to understand +# yet. That's ok--as long as we know how to use code, we don't have to +# understand everything about it. (Do you understand everything about +# MacOS?) Check out the README for documentation on how to use this code. + +# Of course, if you want to dig into this module, feel free. You can ask a +# teacher about it if you're interested. +# ============================================================================= + +from itertools import cycle +from turtle import Turtle, pendown, penup, pencolor + +class Segmenter: + """ + Breaks a distance (length) into segments, which are yielded one at a time. + Whatever's left over at the end gets yielded too. If start_at is given, + the pattern is offset by this much. For example: + + >>> from drawing.lines import Segmenter + >>> list(Segmenter([1, 5]).segment(20)) + [1, 5, 1, 5, 1, 5, 1, 1] + """ + + def __init__(self, pattern): + "Should be initialized with a pattern like [(10, penup), (20, pendown)]" + self.pattern = pattern + self.remainder = 0 + self.remainder_state = None + self.pattern_cycle = cycle(pattern) + + def segment(self, length): + """ + Segments `length` into chunks according to the pattern, yielding each chunk + along with a boolean indicating whether there is more coming + """ + if self.remainder > 0: + if length > self.remainder: + yield self.remainder, self.remainder_state + length -= self.remainder + self.remainder = 0 + else: + yield length, self.remainder_state + self.remainder -= length + length = 0 + if length > 0: + for (seg, state) in self.pattern_cycle: + if length >= seg: + yield seg, state + length -= seg + else: + if length > 0: + yield length, state + self.remainder = seg - length + self.remainder_state = state + return + +def go_segmented(turtle, distance): + "This is the fake go function that we're going to inject into the turtle" + for seg, state in turtle.segmenter.segment(distance): + state() + turtle.true_go(seg) + +def color_setter_factory(color): + "Returns a function that sets the pencolor" + def set_color(): + pencolor(color) + return set_color + +class dashes: + """ + A context manager which causes a code block to draw with dashes. + + Arguments: + spacing (int): (Optional) The length of each dash and space in pixels. Defaults to 20. + + :: + + from superturtle.stroke import dashes + with dashes(): + for side in range(4): + forward(100) + right(90) + """ + def __init__(self, spacing=20): + self.spacing = spacing + + def __enter__(self): + Turtle.segmenter = Segmenter([(self.spacing, pendown), (self.spacing, penup)]) + Turtle.true_go = Turtle._go + Turtle._go = go_segmented + + def __exit__(self, exc_type, exc_value, traceback): + Turtle._go = Turtle.true_go + del Turtle.true_go + +class dots: + """A context manager which causes a code block to draw with dots. + + Arguments: + spacing (int): (Optional) The space between each dot in pixels. Defaults to 10. + + :: + + from turtle import forward, right + from superturtle.stroke import dots + with dots(): + for side in range(5): + forward(100) + right(360/5) + """ + def __init__(self, spacing=10): + self.spacing = spacing + + def __enter__(self): + Turtle.segmenter = Segmenter([(1, pendown), (self.spacing, penup)]) + Turtle.true_go = Turtle._go + Turtle._go = go_segmented + + def __exit__(self, exc_type, exc_value, traceback): + Turtle._go = Turtle.true_go + del Turtle.true_go + +class rainbow: + """A context manager which causes a code block to draw in rainbow colors. + + Arguments: + spacing (int): (Optional) The length of each color segment. Defaults to 10. + colors (list): (Optional) A sequence of color names (any valid inputs to + turtle.setcolor). By default, uses a rainbow. + + :: + + from turtle import forward + from superturtle.stroke import rainbow + with rainbow(colors=["black", "grey", "white"]): + forward(60) + """ + + default_colors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple'] + + def __init__(self, spacing=10, colors=None): + self.spacing = spacing + self.colors = colors or rainbow.default_colors + + def __enter__(self): + Turtle.segmenter = Segmenter([(self.spacing, color_setter_factory(color)) for color in self.colors]) + Turtle.true_go = Turtle._go + Turtle._go = go_segmented + + def __exit__(self, exc_type, exc_value, traceback): + Turtle._go = Turtle.true_go + del Turtle.true_go + + + +# Helpers +# By Chris Proctor +# ---------------- +# A mishmash of useful functions. Feel free to use these in your own projects if they are helpful to you. + +# ============================================================================= +# ! Advanced ! +# ============================================================================= +# This module contains some fancy code that we don't expect you to understand +# yet. That's ok--as long as we know how to use code, we don't have to +# understand everything about it. +# Check out the documentation for documentation on how to use this code. + +# Of course, if you want to dig into this module, feel free. You can ask a +# teacher about it if you're interested. +# ============================================================================= + +from turtle import * +from itertools import chain, cycle + +def fly(x,y): + """Moves the turtle to (x,y) without drawing. + + This is really just a simple shortcut for lifting the pen, + going to a position, and putting the pen down again. But it's such + a commonly-used pattern that it makes sense to put it into a function. + Here's an example:: + + from turtle import forward, right + from superturtle.movement import fly + + def square(size): + for side in range(4): + forward(size) + right(90) + + for col in range(10): + for row in range(10): + fly(40 * col, 40 * row) + square(20) + """ + penup() + goto(x,y) + pendown() + +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() + +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) + +if __name__ == '__main__': + from turtle import forward, right + + with no_delay(): + for i in range(10000): + forward(300) + right(181) + input("That was fast!") + + +# Adapted from https://github.com/semitable/easing-functions +# synchronized names with Penner and added docstrings + +import math + +class EasingBase: + limit = (0, 1) + + def __init__(self, start=0, end=1, duration=1): + self.start = start + self.end = end + self.duration = duration + + @classmethod + def func(cls, t): + raise NotImplementedError + + def ease(self, alpha): + t = self.limit[0] * (1 - alpha) + self.limit[1] * alpha + t /= self.duration + a = self.func(t) + return self.end * a + self.start * (1 - a) + + def __call__(self, alpha): + return self.ease(alpha) + +class linear(EasingBase): + def func(self, t): + return t + +class easeInQuad(EasingBase): + def func(self, t): + return t * t + +class easeOutQuad(EasingBase): + def func(self, t): + return -(t * (t - 2)) + +class easeInOutQuad(EasingBase): + def func(self, t): + if t < 0.5: + return 2 * t * t + return (-2 * t * t) + (4 * t) - 1 + +class easeInCubic(EasingBase): + def func(self, t): + return t * t * t + +class easeOutCubic(EasingBase): + def func(self, t): + return (t - 1) * (t - 1) * (t - 1) + 1 + +class easeInOutCubic(EasingBase): + def func(self, t): + if t < 0.5: + return 4 * t * t * t + p = 2 * t - 2 + return 0.5 * p * p * p + 1 + +class easeInQuartic(EasingBase): + def func(self, t): + return t * t * t * t + +class easeOutQuartic(EasingBase): + def func(self, t): + return (t - 1) * (t - 1) * (t - 1) * (1 - t) + 1 + +class easeInOutQuartic(EasingBase): + def func(self, t): + if t < 0.5: + return 8 * t * t * t * t + p = t - 1 + return -8 * p * p * p * p + 1 + +class easeInQuintic(EasingBase): + def func(self, t): + return t * t * t * t * t + +class easeOutQuintic(EasingBase): + def func(self, t): + return (t - 1) * (t - 1) * (t - 1) * (t - 1) * (t - 1) + 1 + +class easeInOutQuintic(EasingBase): + def func(self, t): + if t < 0.5: + return 16 * t * t * t * t * t + p = (2 * t) - 2 + return 0.5 * p * p * p * p * p + 1 + +class easeInSine(EasingBase): + def func(self, t): + return math.sin((t - 1) * math.pi / 2) + 1 + +class easeOutSine(EasingBase): + def func(self, t): + return math.sin(t * math.pi / 2) + +class easeInOutSine(EasingBase): + def func(self, t): + return 0.5 * (1 - math.cos(t * math.pi)) + +class easeInCirc(EasingBase): + def func(self, t): + return 1 - math.sqrt(1 - (t * t)) + +class easeOutCirc(EasingBase): + def func(self, t): + return math.sqrt((2 - t) * t) + +class easeInOutCirc(EasingBase): + def func(self, t): + if t < 0.5: + return 0.5 * (1 - math.sqrt(1 - 4 * (t * t))) + return 0.5 * (math.sqrt(-((2 * t) - 3) * ((2 * t) - 1)) + 1) + +class easeInExpo(EasingBase): + def func(self, t): + if t == 0: + return 0 + return math.pow(2, 10 * (t - 1)) + +class easeOutExpo(EasingBase): + def func(self, t): + if t == 1: + return 1 + return 1 - math.pow(2, -10 * t) + +class easeInOutExpo(EasingBase): + def func(self, t): + if t == 0 or t == 1: + return t + + if t < 0.5: + return 0.5 * math.pow(2, (20 * t) - 10) + return -0.5 * math.pow(2, (-20 * t) + 10) + 1 + +class easeInElastic(EasingBase): + def func(self, t): + return math.sin(13 * math.pi / 2 * t) * math.pow(2, 10 * (t - 1)) + +class easeOutElastic(EasingBase): + def func(self, t): + return math.sin(-13 * math.pi / 2 * (t + 1)) * math.pow(2, -10 * t) + 1 + +class easeInOutElastic(EasingBase): + def func(self, t): + if t < 0.5: + return ( + 0.5 + * math.sin(13 * math.pi / 2 * (2 * t)) + * math.pow(2, 10 * ((2 * t) - 1)) + ) + return 0.5 * ( + math.sin(-13 * math.pi / 2 * ((2 * t - 1) + 1)) + * math.pow(2, -10 * (2 * t - 1)) + + 2 + ) + +class easeInBack(EasingBase): + def func(self, t): + return t * t * t - t * math.sin(t * math.pi) + +class easeOutBack(EasingBase): + def func(self, t): + p = 1 - t + return 1 - (p * p * p - p * math.sin(p * math.pi)) + +class easeInOutBack(EasingBase): + def func(self, t): + if t < 0.5: + p = 2 * t + return 0.5 * (p * p * p - p * math.sin(p * math.pi)) + p = 1 - (2 * t - 1) + return 0.5 * (1 - (p * p * p - p * math.sin(p * math.pi))) + 0.5 + +class easeInBounce(EasingBase): + def func(self, t): + return 1 - BounceEaseOut().func(1 - t) + +class easeOutBounce(EasingBase): + def func(self, t): + if t < 4 / 11: + return 121 * t * t / 16 + elif t < 8 / 11: + return (363 / 40.0 * t * t) - (99 / 10.0 * t) + 17 / 5.0 + elif t < 9 / 10: + return (4356 / 361.0 * t * t) - (35442 / 1805.0 * t) + 16061 / 1805.0 + return (54 / 5.0 * t * t) - (513 / 25.0 * t) + 268 / 25.0 + +class easeInOutBounce(EasingBase): + def func(self, t): + if t < 0.5: + return 0.5 * BounceEaseIn().func(t * 2) + return 0.5 * BounceEaseOut().func(t * 2 - 1) + 0.5 + + + +# animation.py +# ---------------------- +# By Chris Proctor +# + +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") + +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 interpolate(self, start, stop=None, first_frame=None, last_frame=None, cycles=1, mirror=False, easing=None): + """Interpolates a value between `start` and `stop`. + Interpolation is the process of finding a value partway between two known values. + In this function, the two known values are `start` and `stop`, and we need to find + an appropriate value partway between the two endpoints. When the frame is `first_frame`, + the value should be `start` and when the frame is `last_frame` the value should be `stop`. + When the frame is halfway in between the first and last frames, the value should be halfway + between the endpoints. + Interpolation is used internally by all three of the transformations (rotate, scale, and translate), + but you can use it directly if you want. For example, if you want to scale just one side of a + rectangle:: + + def rectangle(a, b): + for _ in range(2): + forward(a) + right(90) + forward(b) + right(90) + for frame in animate(frames=60): + height = frame.interpolate(20, 80) + width = 100 - height + rectangle(height, width) + + Arguments: + start (int): the initial value. + stop (int): (optional) the final value. If not provided, `start` is returned. + first_frame (int): (optional) The first frame at which interpolation should be + used. If given, the value 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 interpolation should be + used. If given, the value 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 interpolated value reaches `stop` halfway between + `first_frame` and `last_frame`, then returns to `start`. + easing (function): (optional) An easing function to use. + """ + if stop is None: + return start + first_frame = first_frame or 0 + last_frame = last_frame or self.num_frames + if first_frame >= last_frame: + raise AnimationError("last_frame must be greater than first_frame") + period = (last_frame - first_frame) / cycles + ix = min(max(first_frame, self.index), last_frame) + t = ((ix - first_frame) % period) / period + if mirror: + t = 1 - abs(2*t - 1) + if easing is None: + easing = linear + t = easing().ease(t) + return start + t * (stop - start) + +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 + +# image.py +# ---------------------- +# By Chris Proctor +# + +from turtle import getcanvas +from pathlib import Path +from subprocess import run + +def save(filename): + """Saves the canvas as an image. + + Arguments: + filename (str): Location to save the file, including file extension. + """ + temp_file = Path("_temp.eps") + getcanvas().postscript(file=temp_file) + cmd = f"convert {temp_file} -colorspace RGB {filename}" + run(cmd, shell=True, check=True) + temp_file.unlink() +