From 8e47ec5d5abedfcb86b283b86b7ed94999ac6179 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 5 Oct 2024 23:11:08 -0400 Subject: [PATCH] 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. --- scatterplot.py | 42 ++- superturtlescat.py | 831 +++++++++++++++++++++++++++++++++++++++++++++ transform.py | 7 +- 3 files changed, 877 insertions(+), 3 deletions(-) create mode 100644 superturtlescat.py diff --git a/scatterplot.py b/scatterplot.py index 50f66fb..ead2cd7 100644 --- a/scatterplot.py +++ b/scatterplot.py @@ -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() \ No newline at end of file diff --git a/superturtlescat.py b/superturtlescat.py new file mode 100644 index 0000000..2e1efa5 --- /dev/null +++ b/superturtlescat.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() + diff --git a/transform.py b/transform.py index 9513a74..af963ab 100644 --- a/transform.py +++ b/transform.py @@ -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."