The core of the application relies on a `Doodle` class, defined in `doodles.py`.
This provides the basic interface for defining a piece of the drawing.
It is an abstract base class, and comes with 5 implementations:
*`Group`
*`Line`
*`Circle`
*`Rectangle`
*`Text`
Each of these is a concrete class that can be drawn to the screen, whereas the base `Doodle` cannot be.
A doodle is created by defining a `create` method (see any file in `examples/`) that instantitates instances to be drawn.
As objects are created, they are registered with the `World`, which is a global singleton that tracks the complete state of the objects on the screen, and updates them as needed.
`Doodle` (via `World`) delegates the responsibility of actually drawing the objects to a `DrawEngine` to separate out the responsibilities and to make it possible to switch out engines without changing the doodle code.
A recommended order to read the files would be as follows:
0) Run the examples as documented above and take a look at the corresponding code in `examples/`.
1) `doodles.py` - Look at the base `Doodle` class and how `Group` interacts with it to form a tree.
2) `lines.py` - A fully concrete implementation of how an abstract doodle can be made whole. If you want more examples, look to `shapes.py`
3) `draw_engine.py` - The other important base class, where responsibility for drawing is implemented.
4) `world.py` - Contains a lot of implementation code, which can be safely ignored, but useful to see how the engine/doodle interaction is mediated. This is the least important part of the code to fully understand.
-`text.py` and other files are more advanced implementations and can be ignored or saved for last.
The interface created in `draw_engine.py` as well as implementation in `world.py` show the strategy pattern in action.
While this pattern can sometimes be achieved in a functional way by passing a function around, here the drawing strategy is more complex, and so gets aggregated into a class that allows swapping the entire strategy.
An alternate implementation of this class could draw to a PDF or web browser instead of Pygame window.
<https://refactoring.guru/design-patterns/bridge>
At the moment, the `World` and `PygameDrawEngine` are very tightly coupled,
this make sense because they both require `pygame`.
`Circle().move(20, 20).color(255, 0, 0)` does not immediately draw a circle when `move` is called, instead it is informing future draws to include that position. This avoids a non-red circle from being drawn, since the subsequent call to `color(255, 0, 0)` to
also run before the command is executed (via the `draw` method).
Fonts need to be loaded once and are loaded in to shared memory
(currently in `PygameDrawEngine`) instead of having each `Text` object
load the same font.
This is done since it is common to use the same font multiple times in an
application, so having each text object load its own instance of a font would
be immensely wasteful.
This is the most complex part of the code currently, but you can see the logic in the font methods on the DrawEngine and you'll see it is mostly a dictionary mapping
parameters to created objects.
### Others
Some other patterns under consideration would be Observer, Factor, and Visitor.
All of these might have interesting applications in making art this way. (PRs welcome :))
A few more rationales for reason that certain decisions were made, particularly when they were not my first thought.
If any decisions elsewhere in the code are unclear and undocumented, I consider that a bug, please file a GitHub issue.
### Mutability
Often, an API based on chaining like this would be comprised of immutable objects.
Django's QuerySet mostly works in this way.
That was the original intention here as well, but once `Text` was added it was clear that it was going to be a lot more complex than it was worth.
`Text` requires an intermediate buffer to render the text to, and that buffer is then rendered to the screen. This would be incredibly expensive in a pure immutable approach, since there'd be no place to store that state.
The addition of state to the objects simplifies a lot of things, now when an object is created it can be registered with the `World`, if many intermediate copies were created in a chained call like `Circle().pos(100, 100).color(255, 0, 0).z(10).radius(4)` this approach would not be available to us.
### Naming Scheme
This library makes use of @property to create getters, but uses function chaining instead of property-based setters.
This complicates things a bit, since you can't write `self.x(100)` and use `self.x` as a property.
The unorthodox decision was made to use `x(100)` as a setter function, and use `.x_val` as the property name.
This is primarily because setting properties is more common than getting properties in doodles.