Merge pull request #2 from jamesturk/new-anim
New Animations, Polygon, and Liskov Demo
This commit is contained in:
commit
25e1b9d6fe
@ -19,9 +19,9 @@ then there are public/non-public files.)
|
|||||||
"""
|
"""
|
||||||
from .doodles import Doodle, Group
|
from .doodles import Doodle, Group
|
||||||
from .lines import Line
|
from .lines import Line
|
||||||
from .shapes import Circle, Rectangle
|
from .shapes import Circle, Rectangle, Polygon
|
||||||
from .color import Color
|
from .color import Color
|
||||||
from .text import Text
|
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 random
|
||||||
import copy
|
import copy
|
||||||
|
import time
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Callable, Self
|
from typing import Callable, Self, Any
|
||||||
from .color import Color
|
from .color import Color
|
||||||
from .world import world
|
from .world import world
|
||||||
|
|
||||||
|
UpdateCallable = Callable[[float], Any]
|
||||||
|
|
||||||
class Doodle(ABC):
|
class Doodle(ABC):
|
||||||
"""
|
"""
|
||||||
@ -38,7 +40,7 @@ class Doodle(ABC):
|
|||||||
|
|
||||||
# annotations for instance attributes
|
# annotations for instance attributes
|
||||||
_parent: Self | None
|
_parent: Self | None
|
||||||
_updates: list[Callable]
|
_updates: list[tuple[str, UpdateCallable, dict[str, Any]]]
|
||||||
_color: tuple[int, int, int]
|
_color: tuple[int, int, int]
|
||||||
_alpha: int
|
_alpha: int
|
||||||
_z_index: float
|
_z_index: float
|
||||||
@ -110,17 +112,11 @@ class Doodle(ABC):
|
|||||||
new._register()
|
new._register()
|
||||||
return new
|
return new
|
||||||
|
|
||||||
# Dynamic Update Logic ############
|
# animate #######################
|
||||||
|
|
||||||
# These methods relate to a WIP feature
|
def animate(self, prop_name: str, update_func: UpdateCallable, **kwargs) -> Self:
|
||||||
# designed to demonstrate a functional hybrid
|
self._updates.append((prop_name, update_func, kwargs))
|
||||||
# approach to having objects update themselves.
|
return self
|
||||||
#
|
|
||||||
# This feature isn't complete, or documented yet.
|
|
||||||
# TODO
|
|
||||||
|
|
||||||
def register_update(self, method, *args):
|
|
||||||
self._updates.append((method, args))
|
|
||||||
|
|
||||||
def update(self) -> None:
|
def update(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -130,9 +126,19 @@ class Doodle(ABC):
|
|||||||
Can be overriden (see examples.balls)
|
Can be overriden (see examples.balls)
|
||||||
to provide per-object update behavior.
|
to provide per-object update behavior.
|
||||||
"""
|
"""
|
||||||
for method, args in self._updates:
|
cur_time = time.time()
|
||||||
evaled_args = [arg() for arg in args]
|
|
||||||
method(*evaled_args)
|
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 #######################
|
# Setters #######################
|
||||||
|
|
||||||
|
@ -1,9 +1,31 @@
|
|||||||
import time
|
import time
|
||||||
|
import math
|
||||||
from doodles import Circle, Color, Line, Group
|
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():
|
def create():
|
||||||
g = Group().pos(400, 300)
|
g = Group().pos(400, 300)
|
||||||
Circle(g).radius(300).color(Color.BLACK).z(1)
|
Circle(g).color(Color.BLACK).z(1).animate("radius", size_func_factory(260, 50))
|
||||||
Circle(g).radius(290).color(Color.BROWN).z(10)
|
Circle(g).z(10).animate("color", color_func).animate(
|
||||||
|
"radius", size_func_factory(250, 50)
|
||||||
|
)
|
||||||
Circle(g).radius(20).color(Color.BLACK).z(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)
|
self._offset_vec = (x, y)
|
||||||
return self
|
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.
|
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
|
directly (`to`), but there is also an alternate option
|
||||||
that handles commonly used case.
|
that handles commonly used case.
|
||||||
"""
|
"""
|
||||||
if callable(degrees):
|
return self.to(
|
||||||
self.register_update(
|
magnitude * math.cos(math.radians(degrees)),
|
||||||
self.to,
|
magnitude * math.sin(math.radians(degrees)),
|
||||||
lambda: magnitude * math.cos(math.radians(degrees())),
|
)
|
||||||
lambda: magnitude * math.sin(math.radians(degrees())),
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
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(
|
return self.to(
|
||||||
magnitude * math.cos(math.radians(degrees)),
|
magnitude * math.cos(math.radians(degrees)),
|
||||||
magnitude * math.sin(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
|
The interface & decisions are the same but specific
|
||||||
to `Circle` and `Rectangle`.
|
to `Circle` and `Rectangle`.
|
||||||
"""
|
"""
|
||||||
from typing import Self
|
from typing import Self, Optional
|
||||||
import random
|
import random
|
||||||
from .doodles import Doodle
|
from .doodles import Doodle
|
||||||
from .world import world
|
from .world import world
|
||||||
@ -86,3 +86,48 @@ class Rectangle(Doodle):
|
|||||||
return self.width(random.random() * size + 10).height(
|
return self.width(random.random() * size + 10).height(
|
||||||
random.random() * size + 10
|
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"):
|
def line_draw(self, ll: "Line"):
|
||||||
pygame.draw.aaline(self.buffer, ll.rgba, ll.world_vec, ll.end_vec)
|
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
|
# TODO: hard to type, revisit
|
||||||
def text_render(self, text: str, font, color: tuple[int, int, int]):
|
def text_render(self, text: str, font, color: tuple[int, int, int]):
|
||||||
"""returns an intermediated RenderedText"""
|
"""returns an intermediated RenderedText"""
|
||||||
|
Loading…
Reference in New Issue
Block a user