Merge pull request #2 from jamesturk/new-anim
New Animations, Polygon, and Liskov Demo
This commit is contained in:
		
						commit
						25e1b9d6fe
					
				
					 8 changed files with 154 additions and 29 deletions
				
			
		|  | @ -19,9 +19,9 @@ then there are public/non-public files.) | |||
| """ | ||||
| from .doodles import Doodle, Group | ||||
| from .lines import Line | ||||
| from .shapes import Circle, Rectangle | ||||
| from .shapes import Circle, Rectangle, Polygon | ||||
| from .color import Color | ||||
| from .text import Text | ||||
| 
 | ||||
| 
 | ||||
| __all__ = ["Doodle", "Group", "Line", "Circle", "Rectangle", "Color", "Text"] | ||||
| __all__ = ["Doodle", "Group", "Line", "Circle", "Rectangle", "Color", "Text", "Polygon"] | ||||
|  |  | |||
|  | @ -12,11 +12,13 @@ This is a reasonable example of when two classes might reasonable share a file. | |||
| """ | ||||
| import random | ||||
| import copy | ||||
| import time | ||||
| from abc import ABC, abstractmethod | ||||
| from typing import Callable, Self | ||||
| from typing import Callable, Self, Any | ||||
| from .color import Color | ||||
| from .world import world | ||||
| 
 | ||||
| UpdateCallable = Callable[[float], Any] | ||||
| 
 | ||||
| class Doodle(ABC): | ||||
|     """ | ||||
|  | @ -38,7 +40,7 @@ class Doodle(ABC): | |||
| 
 | ||||
|     # annotations for instance attributes | ||||
|     _parent: Self | None | ||||
|     _updates: list[Callable] | ||||
|     _updates: list[tuple[str, UpdateCallable, dict[str, Any]]] | ||||
|     _color: tuple[int, int, int] | ||||
|     _alpha: int | ||||
|     _z_index: float | ||||
|  | @ -110,17 +112,11 @@ class Doodle(ABC): | |||
|         new._register() | ||||
|         return new | ||||
| 
 | ||||
|     # Dynamic Update Logic ############ | ||||
|     # animate ####################### | ||||
| 
 | ||||
|     # These methods relate to a WIP feature | ||||
|     # designed to demonstrate a functional hybrid | ||||
|     # approach to having objects update themselves. | ||||
|     # | ||||
|     # This feature isn't complete, or documented yet. | ||||
|     # TODO | ||||
| 
 | ||||
|     def register_update(self, method, *args): | ||||
|         self._updates.append((method, args)) | ||||
|     def animate(self, prop_name: str, update_func: UpdateCallable, **kwargs) -> Self: | ||||
|         self._updates.append((prop_name, update_func, kwargs)) | ||||
|         return self | ||||
| 
 | ||||
|     def update(self) -> None: | ||||
|         """ | ||||
|  | @ -130,9 +126,19 @@ class Doodle(ABC): | |||
|         Can be overriden (see examples.balls) | ||||
|         to provide per-object update behavior. | ||||
|         """ | ||||
|         for method, args in self._updates: | ||||
|             evaled_args = [arg() for arg in args] | ||||
|             method(*evaled_args) | ||||
|         cur_time = time.time() | ||||
| 
 | ||||
|         for prop, anim_func, kwargs in self._updates: | ||||
|             # attributes on Doodle are set via setter functions | ||||
|             # prop is the name of a setter function, which we | ||||
|             # retrieve here, and then populate with the result | ||||
|             # of anim_func(time) | ||||
|             # kwargs (if set) are passed through directly to anim_func | ||||
|             # allowing constant arguments to be passed as well as | ||||
|             # the variable function-based argument (anim_func) | ||||
|             setter = getattr(self, prop) | ||||
|             new_val = anim_func(cur_time) | ||||
|             setter(new_val, **kwargs) | ||||
| 
 | ||||
|     # Setters ####################### | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,9 +1,31 @@ | |||
| import time | ||||
| import math | ||||
| from doodles import Circle, Color, Line, Group | ||||
| 
 | ||||
| 
 | ||||
| def color_func(t): | ||||
|     cycle = [Color.RED, Color.ORANGE, Color.GREEN, Color.BLUE] | ||||
|     return cycle[int(t) % 4] | ||||
| 
 | ||||
| 
 | ||||
| def size_func_factory(min_size, factor): | ||||
|     def size_function(t): | ||||
|         return math.sin(t) * factor + min_size | ||||
| 
 | ||||
|     return size_function | ||||
| 
 | ||||
| 
 | ||||
| def create(): | ||||
|     g = Group().pos(400, 300) | ||||
|     Circle(g).radius(300).color(Color.BLACK).z(1) | ||||
|     Circle(g).radius(290).color(Color.BROWN).z(10) | ||||
|     Circle(g).color(Color.BLACK).z(1).animate("radius", size_func_factory(260, 50)) | ||||
|     Circle(g).z(10).animate("color", color_func).animate( | ||||
|         "radius", size_func_factory(250, 50) | ||||
|     ) | ||||
|     Circle(g).radius(20).color(Color.BLACK).z(50) | ||||
|     Line(g).vec(lambda: time.time() % 60 / 60 * 360, 200).z(100) | ||||
|     # Line(g).vec( | ||||
|     #     lambda: time.time() % 60 / 60 * 360, | ||||
|     #     200 | ||||
|     # ).z(100) | ||||
| 
 | ||||
|     l = Line(g).vec(0, 200).z(100).animate("degrees", lambda t: t % 60 / 60 * 360) | ||||
|     # l.animate("color", color_func) | ||||
|  |  | |||
							
								
								
									
										25
									
								
								src/doodles/examples/liskov.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/doodles/examples/liskov.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | |||
| """ | ||||
| Demo of the interchangable nature of these classes. | ||||
| """ | ||||
| from doodles import Polygon, Line, Rectangle, Circle, Color | ||||
| import random | ||||
| import math | ||||
| 
 | ||||
| types = [Polygon, Line, Rectangle, Circle] | ||||
| 
 | ||||
| 
 | ||||
| def rainbow(t) -> tuple[int, int, int]: | ||||
|     """cycles through colors based on time""" | ||||
|     t = t % 1.0 | ||||
| 
 | ||||
|     r = int(255 * (1 + math.sin(2 * math.pi * (t + 0.0 / 3))) / 2) | ||||
|     g = int(255 * (1 + math.sin(2 * math.pi * (t + 1.0 / 3))) / 2) | ||||
|     b = int(255 * (1 + math.sin(2 * math.pi * (t + 2.0 / 3))) / 2) | ||||
| 
 | ||||
|     return (r, g, b) | ||||
| 
 | ||||
| 
 | ||||
| def create(): | ||||
|     for _ in range(100): | ||||
|         DoodleType = random.choice(types) | ||||
|         doodle = DoodleType().random().animate("color", rainbow) | ||||
							
								
								
									
										19
									
								
								src/doodles/examples/polygons.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/doodles/examples/polygons.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | |||
| from doodles import Polygon, Color | ||||
| import random | ||||
| 
 | ||||
| 
 | ||||
| def new_point(t): | ||||
|     return (random.random() * 100 - 50, random.random() * 100 - 50) | ||||
| 
 | ||||
| 
 | ||||
| def create(): | ||||
|     for _ in range(5): | ||||
|         p = Polygon().random(3) | ||||
|         for pt in range(3): | ||||
|             p.animate("point", new_point, to_modify=pt) | ||||
|         p = Polygon().random(8) | ||||
|         for pt in range(8): | ||||
|             p.animate("point", new_point, to_modify=pt) | ||||
|         p = Polygon().random(100) | ||||
|         for pt in range(100): | ||||
|             p.animate("point", new_point, to_modify=pt) | ||||
|  | @ -63,7 +63,7 @@ class Line(Doodle): | |||
|         self._offset_vec = (x, y) | ||||
|         return self | ||||
| 
 | ||||
|     def vec(self, degrees: float | Callable, magnitude: float): | ||||
|     def vec(self, degrees: float, magnitude: float): | ||||
|         """ | ||||
|         Alternate setter, to create offset vector from angle & length. | ||||
| 
 | ||||
|  | @ -72,14 +72,16 @@ class Line(Doodle): | |||
|         directly (`to`), but there is also an alternate option | ||||
|         that handles commonly used case. | ||||
|         """ | ||||
|         if callable(degrees): | ||||
|             self.register_update( | ||||
|                 self.to, | ||||
|                 lambda: magnitude * math.cos(math.radians(degrees())), | ||||
|                 lambda: magnitude * math.sin(math.radians(degrees())), | ||||
|             ) | ||||
|             return self | ||||
|         return self.to( | ||||
|             magnitude * math.cos(math.radians(degrees)), | ||||
|             magnitude * math.sin(math.radians(degrees)), | ||||
|         ) | ||||
| 
 | ||||
|     def degrees(self, degrees: float): | ||||
|         """ | ||||
|         Alternate setter, like calling vec(new_degrees, old_magnitude). | ||||
|         """ | ||||
|         magnitude = math.sqrt(self._offset_vec[0] ** 2 + self._offset_vec[1] ** 2) | ||||
|         return self.to( | ||||
|             magnitude * math.cos(math.radians(degrees)), | ||||
|             magnitude * math.sin(math.radians(degrees)), | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ these classes only differ from `Line` in implementation. | |||
| The interface & decisions are the same but specific | ||||
| to `Circle` and `Rectangle`. | ||||
| """ | ||||
| from typing import Self | ||||
| from typing import Self, Optional | ||||
| import random | ||||
| from .doodles import Doodle | ||||
| from .world import world | ||||
|  | @ -86,3 +86,48 @@ class Rectangle(Doodle): | |||
|         return self.width(random.random() * size + 10).height( | ||||
|             random.random() * size + 10 | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| class Polygon(Doodle): | ||||
|     """ | ||||
|     All points are *relative* to the center point. | ||||
|     That is to say, if you had a triangle with coordinates: | ||||
| 
 | ||||
|         (100, 100) | ||||
|         (0, 100) | ||||
|         (-100, 0) | ||||
| 
 | ||||
|     And the object was moved to (50, 50), the actual triangle drawn | ||||
|     on screen would be: | ||||
| 
 | ||||
|         (150, 150) | ||||
|         (50, 150) | ||||
|         (50, 50) | ||||
|     """ | ||||
| 
 | ||||
|     _points: list[tuple[float, float]] | ||||
| 
 | ||||
|     def __init__(self, parent=None): | ||||
|         super().__init__(parent) | ||||
|         self._points = [] | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return f"Polygon(pos={self.world_vec}, points={self._points})" | ||||
| 
 | ||||
|     def draw(self): | ||||
|         world.draw_engine.polygon_draw(self) | ||||
| 
 | ||||
|     def point( | ||||
|         self, point: tuple[float, float], to_modify: Optional[int] = None | ||||
|     ) -> Self: | ||||
|         if to_modify is not None: | ||||
|             self._points[to_modify] = point | ||||
|         else: | ||||
|             self._points.append(point) | ||||
|         return self | ||||
| 
 | ||||
|     def random(self, n_points: int = 5) -> Self: | ||||
|         super().random() | ||||
|         for _ in range(n_points): | ||||
|             self.point((random.random() * 100 - 50, random.random() * 100 - 50)) | ||||
|         return self | ||||
|  |  | |||
|  | @ -90,6 +90,12 @@ class PygameDrawEngine(DrawEngine): | |||
|     def line_draw(self, ll: "Line"): | ||||
|         pygame.draw.aaline(self.buffer, ll.rgba, ll.world_vec, ll.end_vec) | ||||
| 
 | ||||
|     def polygon_draw(self, p: "Polygon"): | ||||
|         # calculate offset points from center of world | ||||
|         offset_points = [(x + p.world_x, y + p.world_y) for (x, y) in p._points] | ||||
|         # draw using anti-aliased lines | ||||
|         pygame.draw.aalines(self.buffer, p.rgba, closed=True, points=offset_points) | ||||
| 
 | ||||
|     # TODO: hard to type, revisit | ||||
|     def text_render(self, text: str, font, color: tuple[int, int, int]): | ||||
|         """returns an intermediated RenderedText""" | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue
	
	 James Turk
						James Turk