|
3 | 3 | import tkinter as tk |
4 | 4 | import operator |
5 | 5 | import time |
6 | | -from typing import NamedTuple, Callable, TypeVar, Generator, Tuple |
| 6 | +import math |
| 7 | +from typing import NamedTuple, Callable, TypeVar, Generator |
7 | 8 | from enum import Enum |
8 | 9 | from dataclasses import dataclass |
9 | | -from functools import partialmethod, partial |
| 10 | +from functools import partialmethod |
| 11 | + |
| 12 | + |
| 13 | +class Animater: |
| 14 | + """ |
| 15 | + Manager for executing animations. |
| 16 | +
|
| 17 | + example:: |
| 18 | +
|
| 19 | + ``` |
| 20 | + motion = Motion(...) |
| 21 | + window = Animater(...) |
| 22 | + window.add_motion(motion) |
| 23 | + ``` |
| 24 | + """ |
| 25 | + _motions = set() |
| 26 | + |
| 27 | + def __init__(self, canvas: tk.Canvas): |
| 28 | + self.canvas = canvas |
| 29 | + |
| 30 | + def start(self): |
| 31 | + while self._motions: |
| 32 | + self.run() |
| 33 | + |
| 34 | + def run(self): |
| 35 | + for motion in self._motions.copy(): |
| 36 | + try: |
| 37 | + move = next(motion) |
| 38 | + move() |
| 39 | + except StopIteration: |
| 40 | + self._motions.remove(motion) |
| 41 | + |
| 42 | + def add(self, motion: Motion): |
| 43 | + self._motions.add(motion.start()) |
| 44 | + |
| 45 | + def add_motion(self, id: int, end: Coord, **kwargs): |
| 46 | + motion = Motion(self.canvas, id, end, **kwargs) |
| 47 | + self.add(motion) |
| 48 | + |
| 49 | + def clear(self): |
| 50 | + self._motions.clear() |
| 51 | + |
| 52 | + @property |
| 53 | + def running(self): |
| 54 | + return bool(self._motions) |
| 55 | + |
| 56 | + |
| 57 | +@dataclass |
| 58 | +class Motion: |
| 59 | + """ |
| 60 | +
|
| 61 | + """ |
| 62 | + canvas: tk.Canvas |
| 63 | + id: int |
| 64 | + end: Coord |
| 65 | + |
| 66 | + speed: int = 1 |
| 67 | + |
| 68 | + def __iter__(self): |
| 69 | + return self.start() |
| 70 | + |
| 71 | + def __key(self): |
| 72 | + return self.id |
| 73 | + |
| 74 | + def __hash__(self): |
| 75 | + return hash(self.__key()) |
| 76 | + |
| 77 | + def __eq__(self): |
| 78 | + return isinstance(self, type(other)) and self.__key() == other.__key() |
| 79 | + |
| 80 | + def start(self) -> Generator[Callable]: |
| 81 | + """ |
| 82 | + The entry point for generating move commands. |
| 83 | + """ |
| 84 | + self.time = time.time() |
| 85 | + self.start = self.current |
| 86 | + self.distance = self.start.distance(self.end) |
| 87 | + self.speed = self.speed ** 3 |
| 88 | + while self.current != self.end: |
| 89 | + yield self.move |
| 90 | + |
| 91 | + def move(self): |
| 92 | + self.canvas.move(self.id, *self.increment) |
| 93 | + self.canvas.update_idletasks() |
| 94 | + |
| 95 | + @property |
| 96 | + def time(self): |
| 97 | + return time.time() - self._time |
| 98 | + |
| 99 | + @time.setter |
| 100 | + def time(self, val): |
| 101 | + self._time = val |
| 102 | + |
| 103 | + @property |
| 104 | + def increment(self): |
| 105 | + mult = (self.time * self.speed) / self.distance |
| 106 | + point = (self.end - self.start) * mult + self.start |
| 107 | + |
| 108 | + if point.distance(self.end) > self.journey: |
| 109 | + return self.end - self.current |
| 110 | + else: |
| 111 | + return point - self.current |
| 112 | + |
| 113 | + @property |
| 114 | + def current(self): |
| 115 | + return Coord(*self.canvas.coords(self.id)) |
| 116 | + |
| 117 | + @property |
| 118 | + def journey(self): |
| 119 | + return self.current.distance(self.end) |
10 | 120 |
|
11 | 121 |
|
12 | 122 | class Coord(NamedTuple): |
@@ -60,18 +170,18 @@ def midpoint(self, other: Coord) -> Coord: |
60 | 170 | """ |
61 | 171 | return (self + other) / 2 |
62 | 172 |
|
63 | | - def distance(self, other: Coord) -> int: |
| 173 | + def distance(self, other: Coord) -> float: |
64 | 174 | """ |
65 | | - The Manhattan distance between `self` and `other`. |
| 175 | + The distance between `self` and `other`. |
66 | 176 |
|
67 | 177 | param: |
68 | 178 | other: Coord -- THe point to consider. |
69 | 179 |
|
70 | 180 | return: |
71 | 181 | int -- A numeric representation of the distance between two points. |
72 | 182 | """ |
73 | | - dist = map(abs, other - self) |
74 | | - return sum(dist) |
| 183 | + diff = other - self |
| 184 | + return math.hypot(*diff) |
75 | 185 |
|
76 | 186 | __add__ = partialmethod(__apply, operator.add) |
77 | 187 | __sub__ = partialmethod(__apply, operator.sub) |
@@ -111,127 +221,3 @@ def __add__(self, other: Direction) -> Coord: |
111 | 221 |
|
112 | 222 | def flip(self): |
113 | 223 | return Direction(Coord(0, 0) - self.value) |
114 | | - |
115 | | - |
116 | | -class Animater: |
117 | | - """ |
118 | | - Manager for executing animations. |
119 | | -
|
120 | | - example:: |
121 | | -
|
122 | | - ``` |
123 | | - motion = Motion(...) |
124 | | - window = Animater(...) |
125 | | - window.add_motion(motion) |
126 | | - ``` |
127 | | - """ |
128 | | - _motions = set() |
129 | | - fps = 60 |
130 | | - |
131 | | - def __init__(self, canvas: tk.Canvas): |
132 | | - self.canvas = canvas |
133 | | - |
134 | | - def start(self): |
135 | | - while self._motions: |
136 | | - time.sleep(1/self.fps) |
137 | | - self.run() |
138 | | - |
139 | | - def run(self): |
140 | | - for motion in self._motions: |
141 | | - try: |
142 | | - next(motion)() |
143 | | - self.canvas.update() |
144 | | - except StopIteration: |
145 | | - self._motions.remove(motion) |
146 | | - break |
147 | | - |
148 | | - def add(self, motion: Motion): |
149 | | - self._motions.add(iter(motion)) |
150 | | - |
151 | | - def add_motion(self, id: int, end: Coord, **kwargs): |
152 | | - motion = Motion(self.canvas, id, (end,), **kwargs) |
153 | | - self.add(motion) |
154 | | - |
155 | | - def clear(self): |
156 | | - self._motions.clear() |
157 | | - |
158 | | - |
159 | | -@dataclass |
160 | | -class Motion: |
161 | | - """ |
162 | | - Defines the movements derived from a generated vector. |
163 | | - The result is a two dimensional generator: the first dimension yields |
164 | | - a "frame" generator, which in turn yields move commands. This structure allows |
165 | | - for different `speed`s of motion, as the length of the second |
166 | | - dimension is determined by `speed`. In other words, the `speed` determines |
167 | | - how many movements occur in one frame. |
168 | | -
|
169 | | - param: |
170 | | - canvas: tk.Canvas -- The parent canvas to issue the move command with. |
171 | | - id: int -- The id of the widget to be animated. |
172 | | - endpoints: Tuple[Coord] -- The final position(s) of the widget. Multiple positions allow for |
173 | | - more intricate pathing. |
174 | | - speed: int (optional) -- The multipler for move commands per frame. |
175 | | - Defaults to 1. |
176 | | -
|
177 | | - example:: |
178 | | -
|
179 | | - ``` |
180 | | - root = tk.Tk() |
181 | | -
|
182 | | - window = Animater(root) |
183 | | - window.pack() |
184 | | -
|
185 | | - c1 = Coord(50, 55) |
186 | | - c2 = Coord(60, 65) |
187 | | - rect = window.create_rectangle(c1, c2) |
188 | | -
|
189 | | - end = c1 + Direction.RIGHT * 50 |
190 | | - end2 = end + Direction.DOWN * 50 |
191 | | - end3 = end2 + (Direction.UP + Direction.LEFT) * 50 |
192 | | -
|
193 | | - animation = Motion(window, rect, (end, end2, end3), speed=1) |
194 | | -
|
195 | | - window.add_motion(animation) |
196 | | - window.add_event('<B1-Motion>') |
197 | | -
|
198 | | - root.mainloop() |
199 | | - ``` |
200 | | - """ |
201 | | - canvas: tk.Canvas |
202 | | - id: int |
203 | | - endpoints: Tuple[Coord] |
204 | | - |
205 | | - speed: int = 1 |
206 | | - |
207 | | - def start(self) -> Generator[Callable]: |
208 | | - """ |
209 | | - The entry point for generating move commands. |
210 | | - """ |
211 | | - move = partial(self.canvas.move, self.id) |
212 | | - |
213 | | - def frame(increment: Coord, count: int): |
214 | | - for _ in range(count): |
215 | | - move(*increment) |
216 | | - self.canvas.master.update_idletasks() |
217 | | - |
218 | | - for end in self.endpoints: |
219 | | - start = Coord(*self.canvas.coords(self.id)[:2]) |
220 | | - steps = round(start.distance(end) / self.speed) |
221 | | - frames = round(steps / self.speed) |
222 | | - increment = (end - start) / steps |
223 | | - |
224 | | - for _ in range(frames): |
225 | | - yield partial(frame, increment, round(steps / frames)) |
226 | | - |
227 | | - def __iter__(self): |
228 | | - return self.start() |
229 | | - |
230 | | - def __key(self): |
231 | | - return self.id |
232 | | - |
233 | | - def __hash__(self): |
234 | | - return hash(self.__key()) |
235 | | - |
236 | | - def __eq__(self): |
237 | | - return isinstance(self, type(other)) and self.__key() == other.__key() |
0 commit comments