22
33import tkinter as tk
44import operator
5- from math import log2
6- from typing import NamedTuple , Callable , TypeVar , List
7- from enum import Flag
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
89from dataclasses import dataclass
9- from functools import partialmethod
10+ from functools import partialmethod , partial
1011
1112
1213class Coord (NamedTuple ):
@@ -17,34 +18,29 @@ class Coord(NamedTuple):
1718 it to the x and y values individually.
1819
1920 param:
20- x: int -- X position.
21- y: int -- Y position.
21+ x: float -- X position.
22+ y: float -- Y position.
2223
2324 example::
2425
2526 ```
2627 c1 = c2 = Coord(1, 1)
2728 c1 + c2
2829 >>> Coord(2, 2)
29- # For convenience, integers are accepted as well
30+ # For convenience, numbers are accepted as well
3031 c1 = Coord(1, 1)
3132 c1 + 1 # 1 is cast to Coord(1, 1)
3233 >>> Coord(2, 2)
3334 ```
34-
35- note:
36-
37- True divide `round`s in order to remain compatible with tkinter
38- coordinate values (`int`).
3935 """
4036
41- x : int
42- y : int
37+ x : float
38+ y : float
4339
44- Operand = TypeVar ('Operand' , 'Coord' , int )
40+ Operand = TypeVar ('Operand' , 'Coord' , float )
4541
4642 def __apply (self , op : Callable , other : Coord .Operand ) -> Coord :
47- if isinstance (other , int ):
43+ if not isinstance (other , Coord ):
4844 other = Coord (other , other )
4945
5046 x = op (self .x , other .x )
@@ -76,28 +72,23 @@ def distance(self, other: Coord) -> int:
7672 dist = map (abs , other - self )
7773 return sum (dist )
7874
79- def __truediv__ (self , other ):
80- result = self .__apply (operator .truediv , other )
81- return Coord (* map (round , result ))
82-
8375 __add__ = partialmethod (__apply , operator .add )
8476 __sub__ = partialmethod (__apply , operator .sub )
8577 __mul__ = partialmethod (__apply , operator .mul )
8678 __mod__ = partialmethod (__apply , operator .mod )
8779 __pow__ = partialmethod (__apply , operator .pow )
8880 __floordiv__ = partialmethod (__apply , operator .floordiv )
81+ __truediv__ = partialmethod (__apply , operator .truediv )
8982
9083
91- class Direction (Flag ):
84+ class Direction (Enum ):
9285 """
9386 Defines base directions.
94-
95-
9687 """
9788 LEFT = Coord (- 1 , 0 )
9889 RIGHT = Coord (1 , 0 )
99- UP = Coord (0 , 1 )
100- DOWN = Coord (0 , - 1 )
90+ UP = Coord (0 , - 1 )
91+ DOWN = Coord (0 , 1 )
10192
10293 def __mul__ (self , other : int ) -> Coord :
10394 return self .value * other
@@ -106,36 +97,100 @@ def __add__(self, other: Direction) -> Coord:
10697 return self .value + other .value
10798
10899
109- class Window (tk .Canvas ):
100+ class Animater (tk .Canvas ):
110101
111- events = []
102+ motions = []
112103
113- def set_event (self , event : tk .EventType ):
104+ def add_event (self , event : tk .EventType ):
114105 self .bind (event , self .run )
115106
116107 def run (self , event ):
117- pass
108+ active = []
109+ for motion in self .motions :
110+ with suppress (StopIteration ):
111+ moves = next (motion )
112+ for move in moves :
113+ move ()
114+ self .update_idletasks ()
115+ active .append (motion )
116+ self .motions = active
117+
118+ def add_motion (self , motion : Motion ):
119+ self .motions .append (iter (motion ))
118120
119121
120122@dataclass
121123class Motion :
124+ """
125+ Defines the movements derived from the given vector.
126+ The result is a two dimensional generator: the first dimension yields
127+ a "frame" generator, which in turn yields move commands. This structure allows
128+ for different `speed`s of motion, as the length of the second
129+ dimension is determined by `speed`. In other words, the `speed` determines
130+ how many movements occur in one frame.
131+
132+ param:
133+ canvas: tk.Canvas -- The parent canvas to issue the move command with.
134+ id: int -- The id of the widget to be animated.
135+ endpoints: Tuple[Coord] -- The final position(s) of the widget. Multiple positions allow for
136+ more intricate pathing.
137+ speed: int (optional) -- The multipler for move commands per frame.
138+ Defaults to 1.
139+
140+ example::
122141
142+ ```
143+ root = tk.Tk()
144+
145+ window = Animater(root)
146+ window.pack()
147+
148+ c1 = Coord(50, 55)
149+ c2 = Coord(60, 65)
150+ rect = window.create_rectangle(c1, c2)
151+
152+ end = c1 + Direction.RIGHT * 50
153+ end2 = end + Direction.DOWN * 50
154+ end3 = end2 + (Direction.UP + Direction.LEFT) * 50
155+
156+ animation = Motion(window, rect, (end, end2, end3), speed=1)
157+
158+ window.add_motion(animation)
159+ window.add_event('<B1-Motion>')
160+
161+ root.mainloop()
162+ ```
163+ """
123164 canvas : tk .Canvas
124- direction : Direction
125- speed : int
165+ id : int
166+ endpoints : Tuple [ Coord ]
126167
127- frames = 30
168+ steps = 60
169+ speed : int = 1
128170
171+ def start (self ) -> Generator [Generator [Callable ]]:
172+ """
173+ The entry point for generating move commands.
174+ """
175+ move = partial (self .canvas .move , self .id )
129176
130- class Animate :
177+ def frame (points : List , last : Coord ) -> Generator [Callable ]:
178+ for point in points :
179+ offset = point - last
180+ yield partial (move , * offset )
181+ last = point
131182
132- def __init__ (self , obj : tk .Widget ):
133- self .obj = obj
183+ for end in self .endpoints :
184+ start = Coord (* self .canvas .coords (self .id )[:2 ])
185+ vec = vector (start , end , self .steps )
134186
135- def set_event (self , event : tk .EventType ):
136- self .bind (event , self .run )
187+ last = vec [0 ]
188+ for points in chunked (vec , self .speed ):
189+ yield frame (points , last )
190+ last = points [- 1 ]
137191
138- # def add(self, )
192+ def __iter__ (self ):
193+ return self .start ()
139194
140195
141196def vector (start : Coord , end : Coord , step : int = 60 ) -> List [Coord ]:
0 commit comments