I updated the superturtle file with the easing stuff and I changed the

colors to use (r,g,b) so that the colors used were more consistent.
This commit is contained in:
root 2024-10-10 19:49:38 -04:00
parent 24a976f129
commit e1d0b5aceb
6 changed files with 892 additions and 417 deletions

Binary file not shown.

Binary file not shown.

View File

@ -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)
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)

View File

@ -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)

View File

@ -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()

831
superturtledraw.py Normal file
View File

@ -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()