doodles
This commit is contained in:
parent
be4cdd31d7
commit
fca6d11504
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.pyc
|
148
main.py
148
main.py
@ -1,148 +0,0 @@
|
|||||||
from abc import ABC, abstractmethod
|
|
||||||
import sys
|
|
||||||
import copy
|
|
||||||
import random
|
|
||||||
import math
|
|
||||||
import pygame
|
|
||||||
|
|
||||||
WHITE = (255, 255, 255)
|
|
||||||
BLACK = (0, 0, 0)
|
|
||||||
WIDTH = 800
|
|
||||||
HEIGHT = 600
|
|
||||||
|
|
||||||
|
|
||||||
class Doodle(ABC):
|
|
||||||
def __init__(self):
|
|
||||||
self._pos_vec = (0, 0)
|
|
||||||
self._color = BLACK
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def draw(self, screen) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def copy(self):
|
|
||||||
return copy.copy(self)
|
|
||||||
|
|
||||||
def color(self, r, g, b):
|
|
||||||
self._color = (r, g, b)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def pos(self, x, y):
|
|
||||||
self._pos_vec = (x, y)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def move(self, dx, dy):
|
|
||||||
return self.pos(self.x + dx, self.y + dy)
|
|
||||||
|
|
||||||
def random(self):
|
|
||||||
x = random.random() * WIDTH
|
|
||||||
y = random.random() * HEIGHT
|
|
||||||
r = random.random() * 255
|
|
||||||
g = random.random() * 255
|
|
||||||
b = random.random() * 255
|
|
||||||
return self.pos(x, y).color(r, g, b)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pos_vec(self):
|
|
||||||
return self._pos_vec
|
|
||||||
|
|
||||||
@property
|
|
||||||
def x(self):
|
|
||||||
return self._pos_vec[0]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def y(self):
|
|
||||||
return self._pos_vec[1]
|
|
||||||
|
|
||||||
|
|
||||||
class Line(Doodle):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
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):
|
|
||||||
pygame.draw.line(screen, self._color, self.pos_vec, self.end_vec)
|
|
||||||
|
|
||||||
def to(self, x, y):
|
|
||||||
self._offset_vec = (x, y)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def random(self):
|
|
||||||
super().random()
|
|
||||||
magnitude = random.random() * 100
|
|
||||||
degrees = random.random() * 360
|
|
||||||
return self.vec(degrees, magnitude)
|
|
||||||
|
|
||||||
def vec(self, degrees, magnitude):
|
|
||||||
return self.to(magnitude * math.cos(math.radians(degrees)),
|
|
||||||
magnitude * math.sin(math.radians(degrees)))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def end_vec(self):
|
|
||||||
return (self.pos_vec[0] + self._offset_vec[0],
|
|
||||||
self.pos_vec[1] + self._offset_vec[1],
|
|
||||||
)
|
|
||||||
|
|
||||||
class Group(Doodle):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self._doodles = []
|
|
||||||
|
|
||||||
def draw(self, screen):
|
|
||||||
for d in self._doodles:
|
|
||||||
d.draw(screen)
|
|
||||||
|
|
||||||
def color(self, r, g, b):
|
|
||||||
for d in self._doodles:
|
|
||||||
d.color(r, g, b)
|
|
||||||
|
|
||||||
def copy(self):
|
|
||||||
return copy.deepcopy(self)
|
|
||||||
|
|
||||||
def add(self, doodle):
|
|
||||||
self._doodles.append(doodle)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def move(self, dx, dy):
|
|
||||||
for d in self._doodles:
|
|
||||||
d.move(dx, dy)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def render_scene(screen, background, *drawables):
|
|
||||||
screen.fill(background)
|
|
||||||
for d in drawables:
|
|
||||||
d.draw(screen)
|
|
||||||
|
|
||||||
def main():
|
|
||||||
pygame.init()
|
|
||||||
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
|
||||||
pygame.display.set_caption("Doodles")
|
|
||||||
|
|
||||||
g = Group()
|
|
||||||
for d in range(100, 200, 10):
|
|
||||||
g.add(
|
|
||||||
Line().pos(300, 300).vec(d, 100)
|
|
||||||
)
|
|
||||||
|
|
||||||
g2 = g.copy()
|
|
||||||
g2.move(20, 20)
|
|
||||||
g3 = g.copy()
|
|
||||||
g3.move(100, 100).color(0, 100, 100)
|
|
||||||
g3.add(Line().random().pos(g3.x, g3.y))
|
|
||||||
g3.add(Line().random().pos(g3.x, g3.y))
|
|
||||||
|
|
||||||
while True:
|
|
||||||
for event in pygame.event.get():
|
|
||||||
if event.type == pygame.QUIT:
|
|
||||||
pygame.quit()
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
render_scene(screen, WHITE, g, g2, g3)
|
|
||||||
|
|
||||||
pygame.display.flip()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
134
poetry.lock
generated
134
poetry.lock
generated
@ -1,5 +1,65 @@
|
|||||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.1.7"
|
||||||
|
description = "Composable command line interface toolkit"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
||||||
|
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
description = "Cross-platform colored terminal text."
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
|
files = [
|
||||||
|
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markdown-it-py"
|
||||||
|
version = "3.0.0"
|
||||||
|
description = "Python port of markdown-it. Markdown parsing, done right!"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
|
||||||
|
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
mdurl = ">=0.1,<1.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
|
||||||
|
code-style = ["pre-commit (>=3.0,<4.0)"]
|
||||||
|
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
|
||||||
|
linkify = ["linkify-it-py (>=1,<3)"]
|
||||||
|
plugins = ["mdit-py-plugins"]
|
||||||
|
profiling = ["gprof2dot"]
|
||||||
|
rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
|
||||||
|
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mdurl"
|
||||||
|
version = "0.1.2"
|
||||||
|
description = "Markdown URL utilities"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
|
||||||
|
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pygame"
|
name = "pygame"
|
||||||
version = "2.5.2"
|
version = "2.5.2"
|
||||||
@ -66,7 +126,79 @@ files = [
|
|||||||
{file = "pygame-2.5.2.tar.gz", hash = "sha256:c1b89eb5d539e7ac5cf75513125fb5f2f0a2d918b1fd6e981f23bf0ac1b1c24a"},
|
{file = "pygame-2.5.2.tar.gz", hash = "sha256:c1b89eb5d539e7ac5cf75513125fb5f2f0a2d918b1fd6e981f23bf0ac1b1c24a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.17.2"
|
||||||
|
description = "Pygments is a syntax highlighting package written in Python."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"},
|
||||||
|
{file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
plugins = ["importlib-metadata"]
|
||||||
|
windows-terminal = ["colorama (>=0.4.6)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rich"
|
||||||
|
version = "13.7.1"
|
||||||
|
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7.0"
|
||||||
|
files = [
|
||||||
|
{file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"},
|
||||||
|
{file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
markdown-it-py = ">=2.2.0"
|
||||||
|
pygments = ">=2.13.0,<3.0.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shellingham"
|
||||||
|
version = "1.5.4"
|
||||||
|
description = "Tool to Detect Surrounding Shell"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"},
|
||||||
|
{file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typer"
|
||||||
|
version = "0.12.3"
|
||||||
|
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"},
|
||||||
|
{file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
click = ">=8.0.0"
|
||||||
|
rich = ">=10.11.0"
|
||||||
|
shellingham = ">=1.3.0"
|
||||||
|
typing-extensions = ">=3.7.4.3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.11.0"
|
||||||
|
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"},
|
||||||
|
{file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"},
|
||||||
|
]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.12"
|
python-versions = "^3.12"
|
||||||
content-hash = "34500777ecbd96a198c62609dff3a5f84da563dcb329987f25690e587613541d"
|
content-hash = "62a9e20d34820e32f43e8980e200ee7607f2a717ef39bdc9cd89000777e450ab"
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "drawing"
|
name = "doodles"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Your Name <you@example.com>"]
|
authors = ["Your Name <you@example.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.12"
|
python = "^3.10"
|
||||||
pygame = "^2.5.2"
|
pygame = "^2.5.2"
|
||||||
|
typer = "^0.12.3"
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
0
src/doodles/__init__.py
Normal file
0
src/doodles/__init__.py
Normal file
47
src/doodles/color.py
Normal file
47
src/doodles/color.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
class Color:
|
||||||
|
"""
|
||||||
|
This class is being used as a namespace, similar to an `enum.Enum` class.
|
||||||
|
|
||||||
|
This is done to group many global variables into a namespace for clarity.
|
||||||
|
|
||||||
|
There is no intention for anyone to ever declare an instance of this class.
|
||||||
|
Instead it will be used like Color.BLACK
|
||||||
|
|
||||||
|
Palette Source: https://pico-8.fandom.com/wiki/Palette
|
||||||
|
"""
|
||||||
|
|
||||||
|
BLACK = (0, 0, 0)
|
||||||
|
DARK_BLUE = (29, 43, 83)
|
||||||
|
PURPLE = (126, 37, 83)
|
||||||
|
DARK_GREEN = (0, 135, 81)
|
||||||
|
BROWN = (171, 82, 54)
|
||||||
|
DARK_GREY = (95, 87, 79)
|
||||||
|
LIGHT_GREY = (194, 195, 199)
|
||||||
|
WHITE = (255, 241, 232)
|
||||||
|
RED = (255, 0, 77)
|
||||||
|
ORANGE = (255, 163, 0)
|
||||||
|
YELLOW = (255, 236, 39)
|
||||||
|
GREEN = (0, 228, 54)
|
||||||
|
BLUE = (41, 173, 255)
|
||||||
|
LAVENDER = (131, 118, 156)
|
||||||
|
PINK = (255, 119, 168)
|
||||||
|
LIGHT_PEACH = (255, 204, 170)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def all():
|
||||||
|
colors = list(Color.__dict__.values())
|
||||||
|
return [color for color in colors if isinstance(color, tuple)]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def random():
|
||||||
|
return random.choice(Color.all())
|
||||||
|
|
||||||
|
|
||||||
|
# Another option would be to just have these be bare methods at the `color` module and
|
||||||
|
# encourage use like color.random()
|
||||||
|
|
||||||
|
# The two are equivalent, but this way makes someone accidentally importing `random()`
|
||||||
|
# impossible.
|
313
src/doodles/doodles.py
Normal file
313
src/doodles/doodles.py
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
import random
|
||||||
|
import copy
|
||||||
|
import math
|
||||||
|
import pygame
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from .color import Color
|
||||||
|
from .world import world
|
||||||
|
|
||||||
|
|
||||||
|
class Doodle(ABC):
|
||||||
|
"""
|
||||||
|
A doodle is a set of drawing primitives.
|
||||||
|
|
||||||
|
Each doodle has a position and a color.
|
||||||
|
Default: (0, 0) & black
|
||||||
|
|
||||||
|
Additionally a doodle can have a parent,
|
||||||
|
which forms the basis of a hierarchy between them.
|
||||||
|
|
||||||
|
Doodles are drawn relative to their parent, so if a circle
|
||||||
|
is placed at (100, 100) and has a child point placed at (10, 10)
|
||||||
|
that point would appear at (110, 110).
|
||||||
|
|
||||||
|
Careful attention is paid in this inheritance hierarchy to the
|
||||||
|
Liskov substitution principle.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
self._parent = parent
|
||||||
|
self._color = parent._color if parent else Color.BLACK
|
||||||
|
# Is storing this vector in a tuple the right thing to do?
|
||||||
|
# It might make more sense to store _x and _y, or use
|
||||||
|
# a library's optimized 2D vector implementation.
|
||||||
|
#
|
||||||
|
# All references to _pos_vec are internal to the class,
|
||||||
|
# so it will be trivial to swap this out later.
|
||||||
|
self._pos_vec = (0, 0)
|
||||||
|
if parent:
|
||||||
|
# register with parent for updates
|
||||||
|
self._parent.add(self)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def draw(self, screen) -> None:
|
||||||
|
"""
|
||||||
|
All doodles need to be drawable, but there is no
|
||||||
|
way we can provide an implementation without
|
||||||
|
knowing more about a concrete shape (Circle, Line, etc.)
|
||||||
|
|
||||||
|
We define this interface, and mark it abstract so that
|
||||||
|
derived classes will be forced to conform to it.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def copy(self) -> "Doodle":
|
||||||
|
"""
|
||||||
|
It will be useful to have the ability to obtain a copy
|
||||||
|
of a given doodle to create repetitive designs.
|
||||||
|
|
||||||
|
This method is provided to fit the chained-object pattern
|
||||||
|
that will be used by the rest of the Doodle API.
|
||||||
|
|
||||||
|
Additionally, while a shallow copy is enough for most
|
||||||
|
cases, it will be possible for child classes to override
|
||||||
|
this to opt for a deepcopy or other logic.
|
||||||
|
"""
|
||||||
|
return copy.copy(self)
|
||||||
|
|
||||||
|
def color(self, r: int, g: int, b: int) -> "Doodle":
|
||||||
|
"""
|
||||||
|
Color works as a kind of setter function.
|
||||||
|
|
||||||
|
The only unique part is that it returns self, accomodating the
|
||||||
|
chained object pattern.
|
||||||
|
"""
|
||||||
|
self._color = (r, g, b)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def pos(self, x: float, y: float) -> "Doodle":
|
||||||
|
"""
|
||||||
|
Another setter, just like color.
|
||||||
|
|
||||||
|
As noted above, this encapsulates our storage decision for our 2D vector.
|
||||||
|
"""
|
||||||
|
self._pos_vec = (x, y)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def move(self, dx: float, dy: float) -> "Doodle":
|
||||||
|
"""
|
||||||
|
This shifts the vector by a set amount.
|
||||||
|
|
||||||
|
By calling self.pos() instead of setting the vector again
|
||||||
|
here it will make use of any future validation logic added to that
|
||||||
|
function.
|
||||||
|
"""
|
||||||
|
return self.pos(self._pos_vec[0] + dx, self._pos_vec[1] + dy)
|
||||||
|
|
||||||
|
def random(self) -> "Doodle":
|
||||||
|
"""
|
||||||
|
Randomize the position and color.
|
||||||
|
"""
|
||||||
|
x = random.random() * world.WIDTH
|
||||||
|
y = random.random() * world.HEIGHT
|
||||||
|
r, g, b = Color.random()
|
||||||
|
# again here, we opt to use the setters so that
|
||||||
|
# future extensions to their behavior will be
|
||||||
|
# used by all downstream functions
|
||||||
|
return self.pos(x, y).color(r, g, b)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def x(self) -> float:
|
||||||
|
"""
|
||||||
|
A read-only attribute "doodle.x" that will
|
||||||
|
return the screen position derived from the parent position
|
||||||
|
plus the current object's x component.
|
||||||
|
|
||||||
|
Note the recursion here, parent.x is an instance of doodle.x.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
A.x = 100
|
||||||
|
|--------B.x 10
|
||||||
|
|--------C.x 20
|
||||||
|
|
||||||
|
When drawing object C, parent.x will call B.x, which will call A.x.
|
||||||
|
B.x will return 110, and C.x will therefore return 130.
|
||||||
|
"""
|
||||||
|
if self._parent:
|
||||||
|
return self._parent.x + self._pos_vec[0]
|
||||||
|
return self._pos_vec[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def y(self) -> float:
|
||||||
|
"""
|
||||||
|
See documentation for .x above.
|
||||||
|
"""
|
||||||
|
if self._parent:
|
||||||
|
return self._parent.y + self._pos_vec[1]
|
||||||
|
return self._pos_vec[1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pos_vec(self) -> (float, float):
|
||||||
|
"""
|
||||||
|
Obtain derived position vector as a 2-tuple.
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
It may be desirable to let any object serve as a parent
|
||||||
|
but for now, groups are needed.
|
||||||
|
(This is analagous to files & directories making up a tree hierarchy.)
|
||||||
|
|
||||||
|
This is an example of a design that requires cooperation between
|
||||||
|
two classes.
|
||||||
|
Each Doodle needs a _parent reference, which should only ever
|
||||||
|
be a Group.
|
||||||
|
In turn, each Group has a list of _doodles.
|
||||||
|
|
||||||
|
This design is possible in Python due to light type coupling, but
|
||||||
|
in some languages would be much trickier to pull off.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._doodles = []
|
||||||
|
|
||||||
|
def draw(self, screen):
|
||||||
|
"""
|
||||||
|
Groups, despite being an abstract concept, are drawable.
|
||||||
|
To draw a group is to draw everything in it.
|
||||||
|
|
||||||
|
The draw logic is to just call the draw method of all children.
|
||||||
|
This works because we know that all doodles are guaranteed
|
||||||
|
to have such a method.
|
||||||
|
"""
|
||||||
|
for d in self._doodles:
|
||||||
|
d.draw(screen)
|
||||||
|
|
||||||
|
def copy(self) -> "Group":
|
||||||
|
"""
|
||||||
|
An override.
|
||||||
|
|
||||||
|
We are storing a list, so deep copies are necessary.
|
||||||
|
"""
|
||||||
|
return copy.deepcopy(self)
|
||||||
|
|
||||||
|
def color(self, r: int, g: int, b: int) -> "Doodle":
|
||||||
|
"""
|
||||||
|
Another override.
|
||||||
|
|
||||||
|
Nothing will ever be drawn in the parent
|
||||||
|
color, but we do want to have the set
|
||||||
|
cascade down to child objects.
|
||||||
|
|
||||||
|
We don't cascade pos() calls, why not?
|
||||||
|
"""
|
||||||
|
super().color(r, g, b)
|
||||||
|
for d in self._doodles:
|
||||||
|
d.color(r, g, b)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add(self, doodle: "Doodle") -> "Group":
|
||||||
|
"""
|
||||||
|
The only unique method of this class, allowing us
|
||||||
|
to add objects to the group.
|
||||||
|
|
||||||
|
Note the violation of class boundaries here.
|
||||||
|
"""
|
||||||
|
self._doodles.append(doodle)
|
||||||
|
doodle._parent = self
|
||||||
|
# This assignment is, strictly speaking, a violation of class
|
||||||
|
# boundaries. Sometimes two classes work together
|
||||||
|
# in a way that makes this necessary, as noted above.
|
||||||
|
# In some languages this would be done via a "protected"
|
||||||
|
# attribute, which is a status between public and private
|
||||||
|
# that only lets certain classes access internals.
|
||||||
|
#
|
||||||
|
# In Python, _parent is merely a suggestion,
|
||||||
|
# and since it is likely that the same author wrote both
|
||||||
|
# classes, it is a suggestion that we can safely ignore
|
||||||
|
# if we understand the implications of tightly
|
||||||
|
# binding the implementations of these two classes.
|
||||||
|
return self
|
10
src/doodles/examples/grid.py
Normal file
10
src/doodles/examples/grid.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from doodles.doodles import Group, Line
|
||||||
|
from doodles.layouts import make_grid, copies
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# Make copies, moving each one and modifying the color
|
||||||
|
make_grid(copies(g), 3, 4, 250, 140, x_offset=70, y_offset=20)
|
27
src/doodles/layouts.py
Normal file
27
src/doodles/layouts.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from .world import world
|
||||||
|
|
||||||
|
|
||||||
|
def make_grid(iterable, cols, rows, width, height, *, x_offset=0, y_offset=0):
|
||||||
|
"""
|
||||||
|
Arranges the objects in iterable in a grid with the given parameters.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
doodle = next(iterable)
|
||||||
|
for c in range(cols):
|
||||||
|
for r in range(rows):
|
||||||
|
doodle.pos(width * c + x_offset, height * r + y_offset)
|
||||||
|
world.add(doodle)
|
||||||
|
doodle = next(iterable)
|
||||||
|
except StopIteration:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def copies(doodle):
|
||||||
|
"""
|
||||||
|
Lazily makes an infinite number of copies of a given doodle.
|
||||||
|
|
||||||
|
Can be combined with things like `make_grid` that require
|
||||||
|
an iterable of doodles to repeat.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
yield doodle.copy()
|
36
src/doodles/main.py
Normal file
36
src/doodles/main.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import sys
|
||||||
|
import copy
|
||||||
|
import random
|
||||||
|
import math
|
||||||
|
import pygame
|
||||||
|
import importlib
|
||||||
|
import typer
|
||||||
|
from .world import world
|
||||||
|
|
||||||
|
|
||||||
|
def main(modname: str):
|
||||||
|
pygame.init()
|
||||||
|
world.init()
|
||||||
|
pygame.display.set_caption("Doodles")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# try fully-qualified first
|
||||||
|
mod = importlib.import_module(modname)
|
||||||
|
except ImportError:
|
||||||
|
# fall back to example
|
||||||
|
try:
|
||||||
|
mod = importlib.import_module("doodles.examples." + modname)
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError(f"Tried to import {modname} and doodles.examples.{modname}")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.QUIT:
|
||||||
|
pygame.quit()
|
||||||
|
sys.exit()
|
||||||
|
world.render()
|
||||||
|
pygame.display.flip()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
typer.run(main)
|
71
src/doodles/world.py
Normal file
71
src/doodles/world.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
from .color import Color
|
||||||
|
import pygame
|
||||||
|
|
||||||
|
class World:
|
||||||
|
"""
|
||||||
|
This class is a singleton, only one instance should ever exist.
|
||||||
|
|
||||||
|
A common reason for this is a class that manages a resource of some kind,
|
||||||
|
in this case, our screen.
|
||||||
|
This class needs to track where entities are in relation to the screen,
|
||||||
|
and will hold a reference to a pygame variable that lets it draw to the screen.
|
||||||
|
|
||||||
|
Multiple instances of this class would
|
||||||
|
|
||||||
|
There are two schools of thought about this pattern:
|
||||||
|
|
||||||
|
You can use this pattern as we do here, with no extra code.
|
||||||
|
We instead define an instance variable "world" below, and
|
||||||
|
documentation would show users to use that global variable.
|
||||||
|
It would technically be possible for someone to instantiate
|
||||||
|
a "world2 = World()" instance, but doing so would be admonished
|
||||||
|
in documentation and not supported.
|
||||||
|
|
||||||
|
You could optionally denote this by naming the class _World.
|
||||||
|
|
||||||
|
Others prefer to have code-level enforcement of this policy.
|
||||||
|
There's a nearly infinite number of ways to do this, including
|
||||||
|
simply keeping a global "_instance_created" variable that gets
|
||||||
|
set to a non-None value after first creation, and then
|
||||||
|
raises an exception on invalid use, or with a bit of cleverness
|
||||||
|
returns the single-instance no matter how hard the user
|
||||||
|
tries to create a new one.
|
||||||
|
"""
|
||||||
|
|
||||||
|
WIDTH = 800
|
||||||
|
HEIGHT = 600
|
||||||
|
_instance = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# This logic forms the basis of a check for prior instances.
|
||||||
|
# Code could be added here to explicitly disallow them.
|
||||||
|
if self._instance is None:
|
||||||
|
self._instance = self
|
||||||
|
self._drawables = []
|
||||||
|
self.background_color = Color.WHITE
|
||||||
|
self.screen = None
|
||||||
|
|
||||||
|
def init(self):
|
||||||
|
"""
|
||||||
|
Delayed initialization, can't be run at start
|
||||||
|
but must be run once.
|
||||||
|
"""
|
||||||
|
if self.screen:
|
||||||
|
raise ValueError("Can't initialize world twice!")
|
||||||
|
self.screen = pygame.display.set_mode((world.WIDTH, world.HEIGHT))
|
||||||
|
|
||||||
|
def add(self, drawable):
|
||||||
|
self._drawables.append(drawable)
|
||||||
|
|
||||||
|
def render(self):
|
||||||
|
"""
|
||||||
|
Draw world to screen
|
||||||
|
"""
|
||||||
|
self.screen.fill(self.background_color)
|
||||||
|
for d in self._drawables:
|
||||||
|
d.draw(self.screen)
|
||||||
|
|
||||||
|
|
||||||
|
# our singleton instance
|
||||||
|
world = World()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user