Skip to content
This repository was archived by the owner on Mar 31, 2020. It is now read-only.

Commit b8e1cf5

Browse files
author
Noah Corona
authored
Merge pull request #4 from Zer0897/animation/view
Animation Engine
2 parents 8842847 + 4aeafd5 commit b8e1cf5

File tree

5 files changed

+280
-75
lines changed

5 files changed

+280
-75
lines changed

Pipfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ flake8 = "*"
88
pytest = "*"
99

1010
[packages]
11+
more-itertools = "*"
1112
pillow = "*"
1213
pygame = "*"
1314
aiohttp = "*"

Pipfile.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/animate.py

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
from __future__ import annotations
2+
3+
import tkinter as tk
4+
import operator
5+
from typing import NamedTuple, Callable, TypeVar, List, Generator, Tuple
6+
from contextlib import suppress
7+
from more_itertools import chunked
8+
from enum import Enum
9+
from dataclasses import dataclass
10+
from functools import partialmethod, partial
11+
12+
13+
class Coord(NamedTuple):
14+
"""
15+
Helper class for managing coordinate values.
16+
17+
Coord overloads many of the numeric operators by mapping
18+
it to the x and y values individually.
19+
20+
param:
21+
x: float -- X position.
22+
y: float -- Y position.
23+
24+
example::
25+
26+
```
27+
c1 = c2 = Coord(1, 1)
28+
c1 + c2
29+
>>> Coord(2, 2)
30+
# For convenience, numbers are accepted as well
31+
c1 = Coord(1, 1)
32+
c1 + 1 # 1 is cast to Coord(1, 1)
33+
>>> Coord(2, 2)
34+
```
35+
"""
36+
37+
x: float
38+
y: float
39+
40+
Operand = TypeVar('Operand', 'Coord', float)
41+
42+
def __apply(self, op: Callable, other: Coord.Operand) -> Coord:
43+
if not isinstance(other, self.__class__):
44+
other = self.__class__(other, other)
45+
if isinstance(other, Direction):
46+
other = other.value
47+
48+
x = op(self.x, other.x)
49+
y = op(self.y, other.y)
50+
return self.__class__(x, y)
51+
52+
def midpoint(self, other: Coord) -> Coord:
53+
"""
54+
The Coord that is equal distance from `self` and `other`.
55+
56+
param:
57+
other: Coord -- The point to consider.
58+
59+
return:
60+
Coord -- The resulting coordinate.
61+
"""
62+
return (self + other) / 2
63+
64+
def distance(self, other: Coord) -> int:
65+
"""
66+
The Manhattan distance between `self` and `other`.
67+
68+
param:
69+
other: Coord -- THe point to consider.
70+
71+
return:
72+
int -- A numeric representation of the distance between two points.
73+
"""
74+
dist = map(abs, other - self)
75+
return sum(dist)
76+
77+
__add__ = partialmethod(__apply, operator.add)
78+
__sub__ = partialmethod(__apply, operator.sub)
79+
__mul__ = partialmethod(__apply, operator.mul)
80+
__mod__ = partialmethod(__apply, operator.mod)
81+
__pow__ = partialmethod(__apply, operator.pow)
82+
__floordiv__ = partialmethod(__apply, operator.floordiv)
83+
__truediv__ = partialmethod(__apply, operator.truediv)
84+
85+
86+
class Direction(Enum):
87+
"""
88+
Defines base directions. Can be used to create Coords relative
89+
to a direction.
90+
91+
example::
92+
93+
```
94+
start = Coord(1, 1)
95+
end = start + (Direction.LEFT * 20)
96+
end
97+
>>> Coord(x=-19, y=1)
98+
"""
99+
LEFT = Coord(-1, 0)
100+
RIGHT = Coord(1, 0)
101+
UP = Coord(0, -1)
102+
DOWN = Coord(0, 1)
103+
104+
def __mul__(self, other: int) -> Coord:
105+
return self.value * other
106+
107+
def __add__(self, other: Direction) -> Coord:
108+
if isinstance(other, self.__class__):
109+
return self.value + other.value
110+
else:
111+
return self.value + other
112+
113+
114+
class Animater(tk.Canvas):
115+
"""
116+
Inherits Canvas. Manager for executing animation move commands.
117+
Currently only event base animations are supported.
118+
119+
example::
120+
121+
```
122+
motion = Motion(...)
123+
window = Animator(...)
124+
window.add_motion(motion)
125+
window.add_event('<Enter>')
126+
```
127+
"""
128+
motions = []
129+
130+
def add_event(self, event: tk.EventType):
131+
self.bind(event, self.run)
132+
133+
def run(self, event):
134+
active = []
135+
for motion in self.motions:
136+
with suppress(StopIteration):
137+
moves = next(motion)
138+
for move in moves:
139+
move()
140+
self.update_idletasks()
141+
active.append(motion)
142+
self.motions = active
143+
144+
def add_motion(self, motion: Motion):
145+
self.motions.append(iter(motion))
146+
147+
148+
@dataclass
149+
class Motion:
150+
"""
151+
Defines the movements derived from a generated vector.
152+
The result is a two dimensional generator: the first dimension yields
153+
a "frame" generator, which in turn yields move commands. This structure allows
154+
for different `speed`s of motion, as the length of the second
155+
dimension is determined by `speed`. In other words, the `speed` determines
156+
how many movements occur in one frame.
157+
158+
param:
159+
canvas: tk.Canvas -- The parent canvas to issue the move command with.
160+
id: int -- The id of the widget to be animated.
161+
endpoints: Tuple[Coord] -- The final position(s) of the widget. Multiple positions allow for
162+
more intricate pathing.
163+
speed: int (optional) -- The multipler for move commands per frame.
164+
Defaults to 1.
165+
166+
example::
167+
168+
```
169+
root = tk.Tk()
170+
171+
window = Animater(root)
172+
window.pack()
173+
174+
c1 = Coord(50, 55)
175+
c2 = Coord(60, 65)
176+
rect = window.create_rectangle(c1, c2)
177+
178+
end = c1 + Direction.RIGHT * 50
179+
end2 = end + Direction.DOWN * 50
180+
end3 = end2 + (Direction.UP + Direction.LEFT) * 50
181+
182+
animation = Motion(window, rect, (end, end2, end3), speed=1)
183+
184+
window.add_motion(animation)
185+
window.add_event('<B1-Motion>')
186+
187+
root.mainloop()
188+
```
189+
"""
190+
canvas: tk.Canvas
191+
id: int
192+
endpoints: Tuple[Coord]
193+
194+
steps = 60
195+
speed: int = 1
196+
197+
def start(self) -> Generator[Generator[Callable]]:
198+
"""
199+
The entry point for generating move commands.
200+
"""
201+
move = partial(self.canvas.move, self.id)
202+
203+
def frame(points: List, last: Coord) -> Generator[Callable]:
204+
for point in points:
205+
offset = point - last
206+
yield partial(move, *offset)
207+
last = point
208+
209+
for end in self.endpoints:
210+
start = Coord(*self.canvas.coords(self.id)[:2])
211+
vec = vector(start, end, self.steps)
212+
213+
last = vec[0]
214+
for points in chunked(vec, self.speed):
215+
yield frame(points, last)
216+
last = points[-1]
217+
218+
def __iter__(self):
219+
return self.start()
220+
221+
222+
def vector(start: Coord, end: Coord, step: int = 60) -> List[Coord]:
223+
"""
224+
Creates a list of all the Coords on a straight line from `start` to `end` (inclusive).
225+
226+
param:
227+
start: Coord -- The starting point.
228+
end: Coord -- The end point.
229+
step: int (optional) -- The desired number of points to include. Defaults to 60.
230+
Actual resulting length may vary.
231+
232+
return:
233+
List[Coord] -- All points that fall on the line from start to end.
234+
235+
example::
236+
237+
```
238+
start = Coord(0, 5)
239+
end = Coord(5, 0)
240+
vector(start, end, 5) # ends up being 8
241+
>>> [
242+
Coord(x=0, y=5), Coord(x=1, y=4), Coord(x=1, y=4),
243+
Coord(x=2, y=2), Coord(x=2, y=2), Coord(x=4, y=1),
244+
Coord(x=4, y=1), Coord(x=5, y=0)
245+
]
246+
```
247+
248+
note:
249+
250+
The current implementation recursively finds midpoints to build the line.
251+
This means the resulting length may vary, due to its eager nature.
252+
"""
253+
mid = start.midpoint(end)
254+
instep = round(step / 2)
255+
if instep:
256+
back = vector(start, mid, step=instep)
257+
front = vector(mid, end, step=instep)
258+
return back + front
259+
else:
260+
return [start, end]

src/coord.py

Lines changed: 0 additions & 71 deletions
This file was deleted.

src/test/test_coord.py renamed to src/test/test_animate.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from ..animation import Coord
1+
from ..animate import Coord, vector, Direction
22

33
coord1 = Coord(1, 1)
44
coord2 = Coord(1, 1)
@@ -30,10 +30,25 @@ def test_pow():
3030

3131

3232
def test_truediv():
33-
assert coord1 / coord2 == Coord(1, 1)
34-
assert Coord(2, 2) / 3 == Coord(1, 1)
33+
assert coord1 / Coord(2, 2) == Coord(0.5, 0.5)
34+
assert coord1 / 2 == Coord(0.5, 0.5)
3535

3636

3737
def test_floordiv():
3838
assert coord1 // coord2 == Coord(1, 1)
3939
assert coord1 // 1 == Coord(1, 1)
40+
41+
42+
def test_direction():
43+
assert Direction.UP.value == Direction.UP + Coord(0, 0)
44+
assert Direction.LEFT.value == Direction.LEFT + Coord(0, 0)
45+
assert Direction.RIGHT.value == Direction.RIGHT + Coord(0, 0)
46+
assert Direction.DOWN.value == Direction.DOWN + Coord(0, 0)
47+
48+
49+
def test_vector():
50+
start = Coord(0, 0)
51+
end = start + 50
52+
vec = vector(start, end)
53+
assert vec[0] == start
54+
assert vec[-1] == end

0 commit comments

Comments
 (0)