diff --git a/src/doodles/__init__.py b/src/doodles/__init__.py index e69de29..227f488 100644 --- a/src/doodles/__init__.py +++ b/src/doodles/__init__.py @@ -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] diff --git a/src/doodles/doodles.py b/src/doodles/doodles.py index c1720e7..6779c0e 100644 --- a/src/doodles/doodles.py +++ b/src/doodles/doodles.py @@ -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) diff --git a/src/doodles/examples/balls.py b/src/doodles/examples/balls.py index 35b1000..464e594 100644 --- a/src/doodles/examples/balls.py +++ b/src/doodles/examples/balls.py @@ -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)] diff --git a/src/doodles/examples/circles.py b/src/doodles/examples/circles.py index 0c059ba..681f67f 100644 --- a/src/doodles/examples/circles.py +++ b/src/doodles/examples/circles.py @@ -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) diff --git a/src/doodles/examples/copies.py b/src/doodles/examples/copies.py index 28a8b46..36d7612 100644 --- a/src/doodles/examples/copies.py +++ b/src/doodles/examples/copies.py @@ -1,4 +1,4 @@ -from doodles.doodles import Group, Line, Circle, Color +from doodles import Group, Circle, Color def original(): g = Group() diff --git a/src/doodles/examples/grid.py b/src/doodles/examples/grid.py index 6f7cd10..9d415d3 100644 --- a/src/doodles/examples/grid.py +++ b/src/doodles/examples/grid.py @@ -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) diff --git a/src/doodles/examples/rects.py b/src/doodles/examples/rects.py index eb9c169..b3e0c9b 100644 --- a/src/doodles/examples/rects.py +++ b/src/doodles/examples/rects.py @@ -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) diff --git a/src/doodles/lines.py b/src/doodles/lines.py new file mode 100644 index 0000000..d22d802 --- /dev/null +++ b/src/doodles/lines.py @@ -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], + ) + diff --git a/src/doodles/main.py b/src/doodles/main.py index 69cde13..52d734e 100644 --- a/src/doodles/main.py +++ b/src/doodles/main.py @@ -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() diff --git a/src/doodles/shapes.py b/src/doodles/shapes.py new file mode 100644 index 0000000..7f2407b --- /dev/null +++ b/src/doodles/shapes.py @@ -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 + )