everything works, even fonts

This commit is contained in:
James Turk 2024-04-23 22:07:25 -05:00
parent 39c9c53bdc
commit 04d765059f
3 changed files with 86 additions and 63 deletions

View File

@ -2,12 +2,6 @@ import random
import itertools
from doodles import Group, Circle, Color, Text
# TODO: depending on system these fonts often do not have all the
# necessary characters, find 3 widely available fonts that do
Text.make_font("small", 16, "mono")
Text.make_font("medium", 24, "copperplate")
Text.make_font("large", 48, "papyrus")
# Via ChatGPT
hello_world = [
"Hello, World!", # English

View File

@ -1,85 +1,50 @@
import pygame
from .doodles import Doodle
from .world import world
# TOOD: make configurable
DEFAULT_FONT_SIZE = 24
class Text(Doodle):
# Having each bit of text on the screen load a separate copy
# of its font would be wasteful, since the most common case would
# be for most text to use the same font.
#
# The solution here is to use a class attribute, shared by *all* instances
# of the class.
#
# This is an implementation of the Flyweight design pattern, which
# allows multiple objects to share some state.
#
# This can quickly become a mess if the shared state is mutable,
# note that here, once a font is loaded it does not change.
# This avoids nearly all pitfalls associated with this approach.
_fonts: dict[str, pygame.font.Font] = {}
# this method is attached to the class `Text`, not individual instances
# like normal methods (which take self as their implicit parameter)
@classmethod
def make_font(cls, name, size, font=None, bold=False, italic=False):
"""
The way fonts work in most graphics libraries requires choosing a font
size, as well as any variation (bold, italic) at the time of creation.
It would be nice if we could allow individual Text objects vary these,
but doing so would be much more complex or require significantly more
memory.
"""
if font is None:
font = pygame.font.Font(None, size)
else:
path = pygame.font.match_font(font, bold=bold, italic=italic)
font = pygame.font.Font(path, size)
cls._fonts[name] = font
@classmethod
def get_font(cls, name=None):
if not name:
# None -> default font
# load on demand
if None not in cls._fonts:
cls._fonts[None] = pygame.font.Font(None, DEFAULT_FONT_SIZE)
return cls._fonts[None]
else:
return cls._fonts[name]
def __init__(self, parent=None):
"""
Text will be centered at `pos`
"""
super().__init__(parent)
self._text = ""
self._rendered = None
self._rendered = None # the surface we pre-render the text to
self._font = None
def __repr__(self):
return f"Text(pos={self.pos_vec}, text={self._text}, parent={self._parent})"
def draw(self, screen):
text_rect = self._rendered.get_rect(center=self.world_vec)
screen.blit(self._rendered, text_rect)
def draw(self):
world.draw_engine.text_draw(self)
def text(self, text: str) -> "Doodle":
"""
A setter for the text
A setter for the text.
This is the only place text can change,
so we can pre-render the surface here.
"""
self._text = text
self._render()
return self
def _render(self):
"""
This function needs to set the _rendered property.
_rendered may be relied upon by draw_engine.draw_text.
"""
# text needs to be rendered once on change to be performant
# doing this in draw would be much slower since it is called
# much more often than the text changes
if not self._font:
self._font = self.get_font() # default font
self._rendered = self._font.render(self._text, True, self._color)
return self
self._font = world.draw_engine.get_font() # default font
self._rendered = world.draw_engine.text_render(
self._text, self._font, self._color
)
def font(self, font: str) -> "Doodle":
# TODO: error checking
self._font = self._fonts[font]
self._font = world.draw_engine.get_font(font)
return self

View File

@ -3,12 +3,66 @@ import pygame
# TODO: fix this with a dynamic load
from .draw_engine import DrawEngine
# TODO: make configurable
DEFAULT_FONT_SIZE = 24
class PygameDrawEngine(DrawEngine):
# Having each bit of text on the screen load a separate copy
# of its font would be wasteful, since the most common case would
# be for most text to use the same font.
#
# The solution here is to use a class attribute, shared by *all* instances
# of the class.
#
# This is an implementation of the Flyweight design pattern, which
# allows multiple objects to share some state.
#
# This can quickly become a mess if the shared state is mutable,
# note that here, once a font is loaded it does not change.
# This avoids nearly all pitfalls associated with this approach.
_fonts: dict[str, pygame.font.Font] = {}
# this method is attached to the class `Text`, not individual instances
# like normal methods (which take self as their implicit parameter)
@classmethod
def make_font(cls, name, size, font=None, bold=False, italic=False):
"""
The way fonts work in most graphics libraries requires choosing a font
size, as well as any variation (bold, italic) at the time of creation.
It would be nice if we could allow individual Text objects vary these,
but doing so would be much more complex or require significantly more
memory.
"""
if font is None:
font = pygame.font.Font(None, size)
else:
path = pygame.font.match_font(font, bold=bold, italic=italic)
font = pygame.font.Font(path, size)
cls._fonts[name] = font
@classmethod
def get_font(cls, name=None):
if not name:
# None -> default font
# load on demand
if None not in cls._fonts:
cls._fonts[None] = pygame.font.Font(None, DEFAULT_FONT_SIZE)
return cls._fonts[None]
else:
return cls._fonts[name]
def init(self):
self.screen = pygame.display.set_mode((world.WIDTH, world.HEIGHT))
self.buffer = pygame.Surface((world.WIDTH, world.HEIGHT), pygame.SRCALPHA)
# TODO: depending on system these fonts often do not have all the
# necessary characters, find 3 widely available fonts that do
world.draw_engine.make_font("small", 16, "mono")
world.draw_engine.make_font("medium", 24, "copperplate")
world.draw_engine.make_font("large", 48, "papyrus")
def render(self, background_color: Color, drawables: list["Doodle"]):
self.buffer.fill((*background_color, 255))
for d in sorted(drawables, key=lambda d: d._z_index):
@ -32,6 +86,16 @@ class PygameDrawEngine(DrawEngine):
def line_draw(self, ll: "Line"):
pygame.draw.aaline(self.buffer, ll.rgba, ll.world_vec, ll.end_vec)
def text_render(self, text: str, font: str, color: Color) -> "TODO":
""" returns an intermediated RenderedText """
# TODO: add accessor text_val
return font.render(text, True, color)
def text_draw(self, txt: "Text"):
# this is a tight coupling, intentionally left
text_rect = txt._rendered.get_rect(center=txt.world_vec)
self.buffer.blit(txt._rendered, text_rect)
class World:
"""