improve examples

This commit is contained in:
James Turk 2024-04-22 02:12:59 -05:00
parent 59bad0c25e
commit 3e4a900753
10 changed files with 216 additions and 193 deletions

View File

@ -0,0 +1,6 @@
from .doodles import Doodle, Group
from .lines import Line
from .shapes import Circle, Rectangle
from .color import Color
__all__ = [Doodle, Group, Line, Circle, Rectangle, Color]

View File

@ -1,7 +1,5 @@
import random
import copy
import math
import pygame
from abc import ABC, abstractmethod
from .color import Color
from .world import world
@ -171,94 +169,6 @@ class Doodle(ABC):
return self.x, self.y
class Line(Doodle):
def __init__(self, parent=None):
"""
We keep the same interface as Doodle, to follow the Liskov substitution
principle.
We could add more *optional* arguments, but no more required ones
than the parent class.
"""
super().__init__(parent)
# a line is stored as a position (on the parent class)
# and an offset vector
self._offset_vec = (10, 0)
def __repr__(self):
return f"Line(pos={self.pos_vec}, end={self.end_vec}, {self._color})"
def draw(self, screen):
"""
Implementation of the abstract draw function for the line.
Note: This is a classic violation of single responsibility.
Instead, you could imagine a class like:
class DrawingBackend:
def draw_doodle(doodle_type, doodle): ...
class PygameBackend(DrawingBackend):
def draw_line(...): ...
This would make it possible to attach different
drawing backends, restoring single-responsibility
to the class and gaining flexibility from separating
presentation logic from data manipulation.
"""
pygame.draw.line(screen, self._color, self.pos_vec, self.end_vec)
def to(self, x: float, y: float) -> "Doodle":
"""
A setter for the line's offset vector.
Example usage:
Line().pos(10, 10).to(50, 50)
Makes a line from (10, 10) to (50, 50).
"""
self._offset_vec = (x, y)
return self
def vec(self, degrees: float, magnitude: float):
"""
Alternate constructor, to create offset vector from angle & length.
"""
return self.to(
magnitude * math.cos(math.radians(degrees)),
magnitude * math.sin(math.radians(degrees)),
)
def random(self) -> "Doodle":
"""
Overrides the parent's random, by extending the behavior.
This is an example of the open/closed principle.
We aren't modifying the parent classes' random function
since doing so would be fragile and break if the
parent class added more options.
Instead we just call it, and extend it with additional
randomization.
"""
super().random()
magnitude = random.random() * 100
degrees = random.random() * 360
return self.vec(degrees, magnitude)
@property
def end_vec(self):
"""
Parallel to pos_vec for end of line.
"""
return (
self.x + self._offset_vec[0],
self.y + self._offset_vec[1],
)
class Group(Doodle):
"""
For now, only Group objects can have child doodles.
@ -345,85 +255,3 @@ class Group(Doodle):
# if we understand the implications of tightly
# binding the implementations of these two classes.
return self
class Circle(Doodle):
def __init__(self, parent=None):
"""
This is a less interesting class than Line, but very similar.
"""
super().__init__(parent)
# circle is a position & radius
self._radius = 0
def __repr__(self):
return f"Circle(pos={self.pos_vec}, radius={self._radius}, {self._color}, parent={self._parent}))"
def draw(self, screen):
pygame.draw.circle(screen, self._color, self.pos_vec, self._radius)
def radius(self, r: float) -> "Doodle":
"""
A setter for the circle's radius.
"""
self._radius = r
return self
def grow(self, by: float):
"""
Modify radius by an amount. (Negative to shrink.)
"""
return self.radius(self._radius + by)
def random(self) -> "Doodle":
super().random()
# constrain to 10-100
return self.radius(random.random()*90 + 10)
class Rectangle(Doodle):
def __init__(self, parent=None):
"""
For compatibility with circle, the rectangle is centered at pos
and expands out width/2, height/2 in each cardinal direction.
"""
super().__init__(parent)
self._width = 100
self._height = 100
def __repr__(self):
return f"Rect(pos={self.pos_vec}, width={self._width}, height={self._height}, parent={self._parent})"
def draw(self, screen):
rect = pygame.Rect(
self.x - self._width/2,
self.y - self._height/2,
self._width,
self._height,
)
pygame.draw.rect(screen, self._color, rect)
def width(self, w: float) -> "Doodle":
"""
A setter for the width
"""
self._width = w
return self
def height(self, h: float) -> "Doodle":
"""
A setter for the height
"""
self._height = h
return self
def grow(self, dw: float, dh: float):
"""
Modify radius by an amount. (Negative to shrink.)
"""
return self.width(self._w + dw).height(self._h + dh)
def random(self, upper=100) -> "Doodle":
super().random()
# constrain to 10-100
return self.width(random.random()*upper + 10).height(random.random()*upper + 10)

View File

@ -1,4 +1,4 @@
from doodles.doodles import Group, Circle, Color
from doodles import Circle, Color
import random
from doodles.world import world
@ -17,7 +17,7 @@ Objects without an update method are static.
class Ball(Circle):
def __init__(self):
super().__init__()
self.speed = 0.005 + random.random() * 0.005
self.speed = 9 + random.random() * 5
def update(self):
self.move(0, self.speed)
@ -29,8 +29,8 @@ class Ball(Circle):
class GravityBall(Circle):
def __init__(self):
super().__init__()
self.accel = 0.0000001 # accel per frame
self.speed = random.random() * 0.002
self.accel = 0.5 # accel per frame
self.speed = random.random() * 10
def update(self):
self.speed += self.accel
@ -40,5 +40,5 @@ class GravityBall(Circle):
self.pos(self.x, world.HEIGHT - 10.01)
def create():
balls = [Ball().pos(40*i, 0).radius(10).color(Color.BLUE) for i in range(21)]
grav = [GravityBall().pos(20+40*i, 0).radius(10).color(Color.PURPLE) for i in range(21)]
[Ball().pos(40*i, 0).radius(10).color(Color.BLUE) for i in range(21)]
[GravityBall().pos(20+40*i, 0).radius(10).color(Color.PURPLE) for i in range(21)]

View File

@ -1,9 +1,15 @@
from doodles.doodles import Group, Circle, Color
from doodles.world import world
from doodles import Group, Circle, Color
def color_cycle():
while True:
yield Color.RED
yield Color.ORANGE
yield Color.YELLOW
def create():
color = color_cycle()
g = Group().pos(400, 300)
for r in range(20, 50, 5):
Circle(g).radius(r).color(Color.random()).z_index(-r)
for r in range(60, 150, 10):
Circle(g).radius(r).color(Color.random()).z_index(-r)
for r in range(20, 100, 12):
Circle(g).radius(r).color(next(color)).z_index(-r)
for r in range(100, 250, 12):
Circle(g).radius(r).color(next(color)).z_index(-r)

View File

@ -1,4 +1,4 @@
from doodles.doodles import Group, Line, Circle, Color
from doodles import Group, Circle, Color
def original():
g = Group()

View File

@ -1,14 +1,16 @@
from doodles.doodles import Group, Line
from doodles import Group, Line
from doodles.layouts import make_grid
import random
def same_spiral():
def spirals():
while True:
# Create a group of lines all with same origin, different angles.
g = Group()
for d in range(0, 180, 10):
Line(g).vec(d, 200 - d)
if random.random() > 0.1:
Line(g).vec(d, 200 - d)
yield g
def create():
# Make copies, moving each one and modifying the color
make_grid(same_spiral(), 3, 4, 250, 140, x_offset=70, y_offset=20)
make_grid(spirals(), 3, 4, 250, 140, x_offset=70, y_offset=20)

View File

@ -1,5 +1,5 @@
from doodles.doodles import Group, Rectangle, Color
from doodles import Rectangle
def create():
for _ in range(100):
r = Rectangle().random(250)
Rectangle().random(250)

92
src/doodles/lines.py Normal file
View File

@ -0,0 +1,92 @@
import math
import random
import pygame
from .doodles import Doodle
class Line(Doodle):
def __init__(self, parent=None):
"""
We keep the same interface as Doodle, to follow the Liskov substitution
principle.
We could add more *optional* arguments, but no more required ones
than the parent class.
"""
super().__init__(parent)
# a line is stored as a position (on the parent class)
# and an offset vector
self._offset_vec = (10, 0)
def __repr__(self):
return f"Line(pos={self.pos_vec}, end={self.end_vec}, {self._color})"
def draw(self, screen):
"""
Implementation of the abstract draw function for the line.
Note: This is a classic violation of single responsibility.
Instead, you could imagine a class like:
class DrawingBackend:
def draw_doodle(doodle_type, doodle): ...
class PygameBackend(DrawingBackend):
def draw_line(...): ...
This would make it possible to attach different
drawing backends, restoring single-responsibility
to the class and gaining flexibility from separating
presentation logic from data manipulation.
"""
pygame.draw.aaline(screen, self._color, self.pos_vec, self.end_vec)
def to(self, x: float, y: float) -> "Doodle":
"""
A setter for the line's offset vector.
Example usage:
Line().pos(10, 10).to(50, 50)
Makes a line from (10, 10) to (50, 50).
"""
self._offset_vec = (x, y)
return self
def vec(self, degrees: float, magnitude: float):
"""
Alternate constructor, to create offset vector from angle & length.
"""
return self.to(
magnitude * math.cos(math.radians(degrees)),
magnitude * math.sin(math.radians(degrees)),
)
def random(self) -> "Doodle":
"""
Overrides the parent's random, by extending the behavior.
This is an example of the open/closed principle.
We aren't modifying the parent classes' random function
since doing so would be fragile and break if the
parent class added more options.
Instead we just call it, and extend it with additional
randomization.
"""
super().random()
magnitude = random.random() * 100
degrees = random.random() * 360
return self.vec(degrees, magnitude)
@property
def end_vec(self):
"""
Parallel to pos_vec for end of line.
"""
return (
self.x + self._offset_vec[0],
self.y + self._offset_vec[1],
)

View File

@ -44,7 +44,8 @@ def main(modname: str = None):
else:
load_module(examples[ex_index])
elapsed = last_update = 0
elapsed = 0
clock = pygame.time.Clock()
while True:
for event in pygame.event.get():
@ -58,11 +59,12 @@ def main(modname: str = None):
elif event.key == pygame.K_LEFT:
ex_index = (ex_index - 1) % len(examples)
load_module(examples[ex_index])
elapsed = pygame.time.get_ticks() - last_update
elapsed += clock.tick(FPS)
while elapsed > MS_PER_FRAME:
elapsed -= MS_PER_FRAME
world.tick()
world.render()
#print(clock.get_fps())
pygame.display.flip()

87
src/doodles/shapes.py Normal file
View File

@ -0,0 +1,87 @@
import random
import pygame
from .doodles import Doodle
class Circle(Doodle):
def __init__(self, parent=None):
"""
This is a less interesting class than Line, but very similar.
"""
super().__init__(parent)
# circle is a position & radius
self._radius = 0
def __repr__(self):
return f"Circle(pos={self.pos_vec}, radius={self._radius}, {self._color}, parent={self._parent}))"
def draw(self, screen):
pygame.draw.circle(screen, self._color, self.pos_vec, self._radius)
def radius(self, r: float) -> "Doodle":
"""
A setter for the circle's radius.
"""
self._radius = r
return self
def grow(self, by: float):
"""
Modify radius by an amount. (Negative to shrink.)
"""
return self.radius(self._radius + by)
def random(self) -> "Doodle":
super().random()
# constrain to 10-100
return self.radius(random.random() * 90 + 10)
class Rectangle(Doodle):
def __init__(self, parent=None):
"""
For compatibility with circle, the rectangle is centered at pos
and expands out width/2, height/2 in each cardinal direction.
"""
super().__init__(parent)
self._width = 100
self._height = 100
def __repr__(self):
return f"Rect(pos={self.pos_vec}, width={self._width}, height={self._height}, parent={self._parent})"
def draw(self, screen):
rect = pygame.Rect(
self.x - self._width / 2,
self.y - self._height / 2,
self._width,
self._height,
)
pygame.draw.rect(screen, self._color, rect)
def width(self, w: float) -> "Doodle":
"""
A setter for the width
"""
self._width = w
return self
def height(self, h: float) -> "Doodle":
"""
A setter for the height
"""
self._height = h
return self
def grow(self, dw: float, dh: float):
"""
Modify radius by an amount. (Negative to shrink.)
"""
return self.width(self._w + dw).height(self._h + dh)
def random(self, upper=100) -> "Doodle":
super().random()
# constrain to 10-100
return self.width(random.random() * upper + 10).height(
random.random() * upper + 10
)