generated from mwc/lab_scatter
Superturtle still not importing even though I have it installed,
so I made another superturtle clone python file (superturtlescat.py) for now. I also changed some of the functions to try to get the scatterplot to be drawn properly, but it's still in progress.
This commit is contained in:
parent
1fd5736751
commit
8e47ec5d5a
|
@ -4,7 +4,7 @@
|
|||
# Uses lots of helper functions in other modules to draw a scatter plot.
|
||||
|
||||
from turtle import *
|
||||
from superturtle.movement import no_delay
|
||||
from superturtlescat import *
|
||||
import constants
|
||||
from generate_data import generate_data
|
||||
from ticks import get_tick_values
|
||||
|
@ -35,12 +35,50 @@ def draw_scatterplot(data, size=5, color="black"):
|
|||
|
||||
def draw_axes(data):
|
||||
"Draws the scatter plot's axes."
|
||||
draw_x_axis()
|
||||
x_values = get_x_values(data)
|
||||
xmin, xmax = bounds(x_values)
|
||||
ticks = get_tick_values(xmin, xmax)
|
||||
for tick in ticks:
|
||||
screen_x_position = scale(tick, xmin, xmax, 0, constants.PLOT_WIDTH)
|
||||
draw_x_tick(screen_x_position, tick)
|
||||
draw_y_axis()
|
||||
y_values = get_y_values(data)
|
||||
ymin, ymax = bounds(y_values)
|
||||
ticks = get_tick_values(xmin, xmax)
|
||||
for tick in ticks:
|
||||
screen_y_position = scale(tick, ymin, ymax, 0, constants.PLOT_WIDTH)
|
||||
draw_y_tick(screen_y_position, tick)
|
||||
|
||||
def draw_points(data, color, size):
|
||||
"Draws the scatter plot's points."
|
||||
#For each point in the data:
|
||||
#Get the x and y value from the point.
|
||||
#Find the x-bounds and the y-bounds of the data. You'll need these for scaling.
|
||||
#Find the scaled x-position for the point.
|
||||
#Find the scaled y-position for the point.
|
||||
#Use draw_point(scaled_x, scaled_y, color, size) to draw the point.
|
||||
for something in somethings:
|
||||
draw_point(scaled_x, scaled_y, color, size)
|
||||
x_values = get_x_values(data)
|
||||
xmin, xmax = bounds(x_values)
|
||||
y_values = get_y_values(data)
|
||||
ymin, ymax = bounds(y_values)
|
||||
for x_value in x_values:
|
||||
scaled_x = scale(x_value, xmin, xmax, ymin, ymax)
|
||||
scx = []
|
||||
scx.append(scaled_x)
|
||||
#return scx #return stops shit, gotta fix it
|
||||
for y_value in y_values:
|
||||
scaled_y = scale(y_value, xmin, xmax, ymin, ymax)
|
||||
scy = []
|
||||
scy.append(scaled_y)
|
||||
for cx, cy in scx, scy:
|
||||
|
||||
|
||||
with no_delay():
|
||||
data = generate_data(50, 10, 500, 5, 400, 1000)
|
||||
# data = [[0,0],[2,4],[4,8],[8,16],[16,32]]
|
||||
draw_scatterplot(data, size=5, color="blue")
|
||||
hideturtle()
|
||||
done()
|
||||
done()
|
|
@ -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()
|
||||
|
|
@ -49,8 +49,13 @@ def clamp(value, low, high):
|
|||
return value
|
||||
if value < low:
|
||||
return low
|
||||
if value == low:
|
||||
return low
|
||||
if value > high:
|
||||
return high
|
||||
if value == high:
|
||||
return high
|
||||
|
||||
|
||||
def ratio(value, start, end):
|
||||
"""Returns a number from 0.0 to 1.0, representing how far along value is from start to end.
|
||||
|
@ -62,7 +67,7 @@ def ratio(value, start, end):
|
|||
|
||||
def scale(value, domain_min, domain_max, range_min, range_max):
|
||||
"Given a value within a domain, returns the scaled equivalent within range."
|
||||
return (range_min + ratio(value, domain_min, domain_max) * (range_max - range_min))
|
||||
return (range_min + ((ratio(value, domain_min, domain_max)) * (range_max - range_min)))
|
||||
|
||||
def get_x_values(points):
|
||||
"Returns the first value for each point in points."
|
||||
|
|
Loading…
Reference in New Issue