From 04d765059fcc9ea826d09c32b40c0769fc4a42f2 Mon Sep 17 00:00:00 2001 From: James Turk Date: Tue, 23 Apr 2024 22:07:25 -0500 Subject: [PATCH] everything works, even fonts --- src/doodles/examples/words.py | 6 --- src/doodles/text.py | 79 ++++++++++------------------------- src/doodles/world.py | 64 ++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 63 deletions(-) diff --git a/src/doodles/examples/words.py b/src/doodles/examples/words.py index a1c4651..159e292 100644 --- a/src/doodles/examples/words.py +++ b/src/doodles/examples/words.py @@ -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 diff --git a/src/doodles/text.py b/src/doodles/text.py index 5f71a4c..f433043 100644 --- a/src/doodles/text.py +++ b/src/doodles/text.py @@ -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 diff --git a/src/doodles/world.py b/src/doodles/world.py index 9d8c12e..d8d9e6a 100644 --- a/src/doodles/world.py +++ b/src/doodles/world.py @@ -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: """