From ec7bbad37210aa674600d7097150938fa0a58d99 Mon Sep 17 00:00:00 2001 From: James Turk Date: Tue, 30 Apr 2024 19:56:54 -0500 Subject: [PATCH 1/4] moved animation code to more general interface, and updated clock example --- src/doodles/doodles.py | 33 ++++++++++++++++++--------------- src/doodles/examples/clock.py | 15 ++++++++++++++- src/doodles/lines.py | 18 ++++++++++-------- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/doodles/doodles.py b/src/doodles/doodles.py index 889cdf4..ae98bd3 100644 --- a/src/doodles/doodles.py +++ b/src/doodles/doodles.py @@ -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]] _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) -> Self: + self._updates.append((prop_name, update_func)) + return self def update(self) -> None: """ @@ -130,9 +126,16 @@ 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 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) + setter = getattr(self, prop) + new_val = anim_func(cur_time) + setter(new_val) # Setters ####################### diff --git a/src/doodles/examples/clock.py b/src/doodles/examples/clock.py index 0d75495..ab3445d 100644 --- a/src/doodles/examples/clock.py +++ b/src/doodles/examples/clock.py @@ -1,9 +1,22 @@ import time from doodles import Circle, Color, Line, Group + +def color_func(t): + print("updating color") + cycle = [Color.RED, Color.ORANGE, Color.GREEN, Color.BLUE] + return cycle[int(t) % 4] + + 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).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) diff --git a/src/doodles/lines.py b/src/doodles/lines.py index 66bc4db..88a1281 100644 --- a/src/doodles/lines.py +++ b/src/doodles/lines.py @@ -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)), From e8aff3394e4d58f462b0c38da76793c7f85b842e Mon Sep 17 00:00:00 2001 From: James Turk Date: Tue, 30 Apr 2024 20:06:01 -0500 Subject: [PATCH 2/4] demo that you can animate any property --- src/doodles/examples/clock.py | 17 +++++++++++++---- src/doodles/lines.py | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/doodles/examples/clock.py b/src/doodles/examples/clock.py index ab3445d..acfe4a6 100644 --- a/src/doodles/examples/clock.py +++ b/src/doodles/examples/clock.py @@ -1,17 +1,26 @@ import time +import math from doodles import Circle, Color, Line, Group def color_func(t): - print("updating color") 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, @@ -19,4 +28,4 @@ def create(): # ).z(100) l = Line(g).vec(0, 200).z(100).animate("degrees", lambda t: t % 60 / 60 * 360) - l.animate("color", color_func) + # l.animate("color", color_func) diff --git a/src/doodles/lines.py b/src/doodles/lines.py index 88a1281..623c276 100644 --- a/src/doodles/lines.py +++ b/src/doodles/lines.py @@ -81,7 +81,7 @@ class Line(Doodle): """ Alternate setter, like calling vec(new_degrees, old_magnitude). """ - magnitude = math.sqrt(self._offset_vec[0]**2 + self._offset_vec[1]**2) + 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)), From 8f5b03f34adca3b3363c7cb471ac85fa16be14f0 Mon Sep 17 00:00:00 2001 From: James Turk Date: Tue, 30 Apr 2024 20:56:37 -0500 Subject: [PATCH 3/4] add Polygon --- src/doodles/__init__.py | 4 +-- src/doodles/doodles.py | 13 +++++---- src/doodles/examples/polygons.py | 19 +++++++++++++ src/doodles/shapes.py | 47 +++++++++++++++++++++++++++++++- src/doodles/world.py | 6 ++++ 5 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 src/doodles/examples/polygons.py diff --git a/src/doodles/__init__.py b/src/doodles/__init__.py index 9ef8b52..70332b1 100644 --- a/src/doodles/__init__.py +++ b/src/doodles/__init__.py @@ -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"] diff --git a/src/doodles/doodles.py b/src/doodles/doodles.py index ae98bd3..dfb9b47 100644 --- a/src/doodles/doodles.py +++ b/src/doodles/doodles.py @@ -40,7 +40,7 @@ class Doodle(ABC): # annotations for instance attributes _parent: Self | None - _updates: list[tuple[str, UpdateCallable]] + _updates: list[tuple[str, UpdateCallable, dict[str, Any]]] _color: tuple[int, int, int] _alpha: int _z_index: float @@ -114,8 +114,8 @@ class Doodle(ABC): # animate ####################### - def animate(self, prop_name: str, update_func: UpdateCallable) -> Self: - self._updates.append((prop_name, update_func)) + 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: @@ -128,14 +128,17 @@ class Doodle(ABC): """ cur_time = time.time() - for prop, anim_func in self._updates: + 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) + setter(new_val, **kwargs) # Setters ####################### diff --git a/src/doodles/examples/polygons.py b/src/doodles/examples/polygons.py new file mode 100644 index 0000000..6430b27 --- /dev/null +++ b/src/doodles/examples/polygons.py @@ -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) diff --git a/src/doodles/shapes.py b/src/doodles/shapes.py index 1a4c5be..775e8be 100644 --- a/src/doodles/shapes.py +++ b/src/doodles/shapes.py @@ -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) -> Self: + super().random() + for _ in range(n_points): + self.point((random.random() * 100 - 50, random.random() * 100 - 50)) + return self diff --git a/src/doodles/world.py b/src/doodles/world.py index f02f144..9b43d9a 100644 --- a/src/doodles/world.py +++ b/src/doodles/world.py @@ -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""" From 2bf6e352ec744d0196f0944754c494bb0e9f5c86 Mon Sep 17 00:00:00 2001 From: James Turk Date: Tue, 30 Apr 2024 21:23:23 -0500 Subject: [PATCH 4/4] add liskov demo --- src/doodles/examples/liskov.py | 25 +++++++++++++++++++++++++ src/doodles/shapes.py | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/doodles/examples/liskov.py diff --git a/src/doodles/examples/liskov.py b/src/doodles/examples/liskov.py new file mode 100644 index 0000000..6d2ecab --- /dev/null +++ b/src/doodles/examples/liskov.py @@ -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) diff --git a/src/doodles/shapes.py b/src/doodles/shapes.py index 775e8be..3fb6e96 100644 --- a/src/doodles/shapes.py +++ b/src/doodles/shapes.py @@ -126,7 +126,7 @@ class Polygon(Doodle): self._points.append(point) return self - def random(self, n_points: int) -> 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))