Source code for rubato.utils.rendering.draw

"""A static class for drawing things directly to the window."""
from __future__ import annotations
from typing import Optional, Callable, TYPE_CHECKING
import cython, math

import sdl2, sdl2.ext

from . import Font, Surface
from .. import Vector, Color, Display, InitError, Math, Time

if TYPE_CHECKING:
    from . import Camera


@cython.cclass
class _DrawTask:
    priority: int = cython.declare(int, visibility="public")  # type: ignore
    func: Callable = cython.declare(object, visibility="public")  # type: ignore

    def __init__(self, priority: int, func: Callable):
        self.priority = priority
        self.func = func


# THIS IS A STATIC CLASS
[docs]class Draw: """A static class allowing drawing items to the window.""" _queue: list[_DrawTask] = [] _pt_surfs: dict[Color, Surface] = {} _line_surfs: dict[tuple, Surface] = {} _rect_surfs: dict[tuple, Surface] = {} _circle_surfs: dict[tuple, Surface] = {} _poly_surfs: dict[tuple, Surface] = {} def __init__(self) -> None: raise InitError(self) @staticmethod def _draw_fps(font: Font): """ Draws the current FPS to the screen. Called automatically if `Game.show_fps` is True. Args: font: The font to use. """ height: int = math.ceil(Display.res.y / 32) pad = max(height / 4, 1) scale = height / font.size Draw.text( str(Time.smooth_fps()), font=font, pos=Display.top_left + (pad, -pad), align=Vector(1, 1), justify="center", scale=(scale, scale), shadow=True, shadow_pad=(pad, pad), af=False )
[docs] @classmethod def clear(cls, background_color: Color = Color.white, border_color: Color = Color.black): """ Clears the screen and draws a background. Args: background_color: The background color. Defaults to white. border_color: The border color. Defaults to black. Shown when the aspect ratio of the game does not match the aspect ratio of the window. """ Display.renderer.clear(border_color.to_tuple()) Display.renderer.fill( (0, 0, *Display.renderer.logical_size), background_color.to_tuple(), )
@classmethod def _push(cls, z_index: int, callback: Callable): """ Add a custom draw function to the frame queue. Args: z_index: The z_index to call at (lower z_indexes get called first). callback: The function to call. """ cls._queue.append(_DrawTask(z_index, callback)) @classmethod def _dump(cls): """Draws all queued items. Is called automatically at the end of every frame.""" if not cls._queue: return cls._queue.sort(key=lambda x: x.priority) for task in cls._queue: task.func() cls._queue.clear()
[docs] @classmethod def queue_pixel( cls, pos: Vector | tuple[float, float], color: Color = Color.cyan, z_index: int = 0, camera: Camera | None = None ): """ Draw a point onto the renderer at the end of the frame. Args: pos: The position of the point. color: The color to use for the pixel. Defaults to Color.cyan. z_index: Where to draw it in the drawing order. Defaults to 0. camera: The camera to use. Defaults to None. """ if camera is not None and camera.z_index < z_index: return cls._push(z_index, lambda: cls.pixel(pos, color, camera))
[docs] @classmethod def pixel(cls, pos: Vector | tuple[float, float], color: Color = Color.cyan, camera: Camera | None = None): """ Draw a point onto the renderer immediately. Args: pos: The position of the point. color: The color to use for the pixel. Defaults to Color.cyan. camera: The camera to use. Defaults to None. """ if (surf := cls._pt_surfs.get(color, None)) is None: surf = Surface(1, 1) surf.set_pixel((0, 0), color) cls._pt_surfs[color] = surf cls.surface(surf, pos, camera)
[docs] @classmethod def queue_line( cls, p1: Vector | tuple[float, float], p2: Vector | tuple[float, float], color: Color = Color.cyan, width: int | float = 1, z_index: int = 0, camera: Camera | None = None ): """ Draw a line onto the renderer at the end of the frame. Args: p1: The first point of the line. p2: The second point of the line. color: The color to use for the line. Defaults to Color.cyan. width: The width of the line. Defaults to 1. z_index: Where to draw it in the drawing order. Defaults to 0. camera: The camera to use. Defaults to None. """ if camera is not None and camera.z_index < z_index: return cls._push(z_index, lambda: cls.line(p1, p2, color, width, camera))
[docs] @staticmethod def line( p1: Vector | tuple[float, float], p2: Vector | tuple[float, float], color: Color = Color.cyan, width: int | float = 1, camera: Camera | None = None ): """ Draw a line onto the renderer immediately. Args: p1: The first point of the line. p2: The second point of the line. color: The color to use for the line. Defaults to Color.cyan. width: The width of the line. Defaults to 1. camera: The camera to use. Defaults to None. """ dims = Vector.create(p2) - p1 hashing = dims, color, width if (surf := Draw._line_surfs.get(hashing, None)) is None: pad = round(width) sizex, sizey = abs(round(dims.x)), abs(round(dims.y)) halfx, halfy = sizex / 2, sizey / 2 surf = Surface(sizex + (2 * pad), sizey + (2 * pad)) surf.draw_line( (halfx * Math.sign(-dims.x), halfy * Math.sign(-dims.y)), (halfx * Math.sign(dims.x), halfy * Math.sign(dims.y)), color, thickness=round(width), ) Draw._line_surfs[hashing] = surf Draw.surface(surf, p1 + dims / 2 + round(width), camera)
[docs] @classmethod def queue_rect( cls, center: Vector | tuple[float, float], width: int | float, height: int | float, border: Optional[Color] = Color.cyan, border_thickness: int | float = 1, fill: Optional[Color] = None, angle: float = 0, z_index: int = 0, camera: Camera | None = None ): """ Draws a rectangle onto the renderer at the end of the frame. Args: center: The center of the rectangle. width: The width of the rectangle. height: The height of the rectangle. border: The border color. Defaults to Color.cyan. border_thickness: The border thickness. Defaults to 1. fill: The fill color. Defaults to None. angle: The angle in degrees. Defaults to 0. z_index: Where to draw it in the drawing order. Defaults to 0. camera: The camera to use. Defaults to None. """ if camera is not None and camera.z_index < z_index: return cls._push(z_index, lambda: cls.rect(center, width, height, border, border_thickness, fill, angle, camera))
[docs] @classmethod def rect( cls, center: Vector | tuple[float, float], width: int | float, height: int | float, border: Optional[Color] = Color.cyan, border_thickness: int | float = 1, fill: Optional[Color] = None, angle: float = 0, camera: Camera | None = None ): """ Draws a rectangle onto the renderer immediately. Args: center: The center of the rectangle. width: The width of the rectangle. height: The height of the rectangle. border: The border color. Defaults to Color.cyan. border_thickness: The border thickness. Defaults to 1. fill: The fill color. Defaults to None. angle: The angle in degrees. Defaults to 0. camera: The camera to use. Defaults to None. Raises: ValueError: If the width and height are not positive. """ hashing = width, height, border, border_thickness, fill if width <= 0 or height <= 0: raise ValueError("Width and height must be positive.") if (surf := cls._rect_surfs.get(hashing, None)) is None: pad = round(border_thickness) if border is not None else 0 surf = Surface(pad + round(width), pad + round(height)) surf.draw_rect((0, 0), (width, height), border, pad, fill) cls._rect_surfs[hashing] = surf surf.rotation = angle cls.surface(surf, center, camera)
[docs] @classmethod def queue_circle( cls, center: Vector | tuple[float, float], radius: int = 4, border: Optional[Color] = Color.cyan, border_thickness: int | float = 1, fill: Optional[Color] = None, z_index: int = 0, camera: Camera | None = None ): """ Draws a circle onto the renderer at the end of the frame. Args: center: The center. radius: The radius. Defaults to 4. border: The border color. Defaults to Color.cyan. border_thickness: The border thickness. Defaults to 1. fill: The fill color. Defaults to None. z_index: Where to draw it in the drawing order. Defaults to 0. camera: The camera to use. Defaults to None. """ if camera is not None and camera.z_index < z_index: return cls._push(z_index, lambda: cls.circle(center, radius, border, border_thickness, fill, camera))
[docs] @classmethod def circle( cls, center: Vector | tuple[float, float], radius: int | float = 4, border: Optional[Color] = Color.cyan, border_thickness: int | float = 1, fill: Optional[Color] = None, camera: Camera | None = None ): """ Draws a circle onto the renderer immediately. Args: center: The center. radius: The radius. Defaults to 4. border: The border color. Defaults to Color.cyan. border_thickness: The border thickness. Defaults to 1. fill: The fill color. Defaults to None. camera: The camera to use. Defaults to None. Raises: ValueError: If the radius is not positive. """ hashing = radius, border, border_thickness, fill if radius <= 0: raise ValueError("Radius must be positive.") if (surf := cls._circle_surfs.get(hashing, None)) is None: pad = round(border_thickness) if border is not None else 0 surf = Surface(pad * 2 + round(radius * 2) + 1, pad * 2 + round(radius * 2) + 1) surf.draw_circle((0, 0), round(radius), border, round(border_thickness), fill) cls._circle_surfs[hashing] = surf cls.surface(surf, center, camera)
[docs] @classmethod def queue_poly( cls, points: list[Vector] | list[tuple[float, float]], center: Vector | tuple[float, float], border: Optional[Color] = Color.cyan, border_thickness: int | float = 1, fill: Optional[Color] = None, z_index: int = 0, camera: Camera | None = None ): """ Draws a polygon onto the renderer at the end of the frame. Args: points: The list of points to draw relative to the center. center: The center of the polygon. border: The border color. Defaults to Color.cyan. border_thickness: The border thickness. Defaults to 1. fill: The fill color. Defaults to None. z_index: Where to draw it in the drawing order. Defaults to 0. camera: The camera to use. Defaults to None. """ if camera is not None and camera.z_index < z_index: return cls._push(z_index, lambda: cls.poly(points, center, border, border_thickness, fill, camera))
[docs] @classmethod def poly( cls, points: list[Vector] | list[tuple[float, float]], center: Vector | tuple[float, float], border: Optional[Color] = Color.cyan, border_thickness: int | float = 1, fill: Optional[Color] = None, camera: Camera | None = None ): """ Draws a polygon onto the renderer immediately. Args: points: The list of points to draw relative to the center. center: The center of the polygon. border: The border color. Defaults to Color.cyan. border_thickness: The border thickness. Defaults to 1. fill: The fill color. Defaults to None. camera: The camera to use. Defaults to None. """ hashing = tuple(points), border, border_thickness, fill if (surf := cls._poly_surfs.get(hashing, None)) is None: min_x, min_y = Math.INF, Math.INF max_x, max_y = -Math.INF, -Math.INF for point in points: min_x = min(min_x, point[0]) min_y = min(min_y, point[1]) max_x = max(max_x, point[0]) max_y = max(max_y, point[1]) pad = round(border_thickness) if border is not None else 0 surf = Surface(pad * 2 + round(max_x - min_x + 2), pad * 2 + round(max_y - min_y + 2)) surf.draw_poly(points, (0, 0), border, round(border_thickness), fill) cls._poly_surfs[hashing] = surf cls.surface(surf, center, camera)
[docs] @classmethod def queue_text( cls, text: str, font: Font, pos: Vector | tuple[float, float] = (0, 0), justify: str = "left", align: Vector | tuple[float, float] = (0, 0), width: int | float = 0, scale: Vector | tuple[float, float] = (1, 1), shadow: bool = False, shadow_pad: Vector | tuple[float, float] = (0, 0), af: bool = True, z_index: int = 0, camera: Camera | None = None ): """ Draws some text onto the renderer at the end of the frame. Args: text: The text to draw. font: The Font object to use. pos: The top left corner of the text. Defaults to (0, 0). justify: The justification of the text. (left, center, right). Defaults to "left". align: The alignment of the text. Defaults to (0, 0). width: The maximum width of the text. Will automatically wrap the text. Defaults to -1. scale: The scale of the text. Defaults to (1, 1). shadow: Whether to draw a basic shadow box behind the text. Defaults to False. shadow_pad: What padding to use for the shadow. Defaults to (0, 0). af: Whether to use anisotropic filtering. Defaults to True. z_index: Where to draw it in the drawing order. Defaults to 0. camera: The camera to use. Defaults to None. """ if camera is not None and camera.z_index < z_index: return cls._push( z_index, lambda: cls.text(text, font, pos, justify, align, width, scale, shadow, shadow_pad, af, camera) )
[docs] @classmethod def text( cls, text: str, font: Font, pos: Vector | tuple[float, float] = (0, 0), justify: str = "left", align: Vector | tuple[float, float] = (0, 0), width: int | float = 0, scale: Vector | tuple[float, float] = (1, 1), shadow: bool = False, shadow_pad: Vector | tuple[float, float] = (0, 0), af: bool = True, camera: Camera | None = None ): """ Draws some text onto the renderer immediately. Args: text: The text to draw. font: The Font object to use. pos: The top left corner of the text. Defaults to (0, 0). justify: The justification of the text. (left, center, right). Defaults to "left". align: The alignment of the text. Defaults to (0, 0). width: The maximum width of the text. Will automatically wrap the text. Defaults to -1. scale: The scale of the text. Defaults to (1, 1). shadow: Whether to draw a basic shadow box behind the text. Defaults to False. shadow_pad: What padding to use for the shadow. Defaults to (0, 0). af: Whether to use anisotropic filtering. Defaults to True. camera: The camera to use. Defaults to None. """ shadow_pad = Vector.create(shadow_pad) if camera is not None: pos = camera.transform(pos) scale = camera.zoom * scale[0], camera.zoom * scale[1] shadow_pad = camera.zoom * shadow_pad surf = font._generate(text, justify, width) tx = Surface._from_surf(surf, scale=scale, af=af) sdl2.SDL_FreeSurface(surf) pad_x, pad_y = (shadow_pad / scale).tuple_int() if shadow: tx_dims = tx.width + 2 * pad_x, font.size + 2 * pad_y final_tx = Surface(*tx_dims, scale=scale) final_tx.fill(Color(a=200)) final_tx.blit( tx, (0, 0, tx.width, font.size), ) else: final_tx = tx size = final_tx.size_scaled() center = ( pos[0] + (align[0] * size[0]) / 2, pos[1] - (align[1] * size[1]) / 2, ) cls.surface(final_tx, center, camera)
[docs] @classmethod def queue_surface( cls, surface: Surface, pos: Vector | tuple[float, float] = (0, 0), z_index: int = 0, camera: Camera | None = None ): """ Draws an surface onto the renderer at the end of the frame. Args: surface: The surface to draw. pos: The position to draw the surface at. Defaults to (0, 0). z_index: The z-index of the surface. Defaults to 0. camera: The camera to use. Defaults to None. """ if camera is not None and camera.z_index < z_index: return cls._push(z_index, lambda: cls.surface(surface, pos, camera))
[docs] @classmethod def surface(cls, surface: Surface, pos: Vector | tuple[float, float] = (0, 0), camera: Camera | None = None): """ Draws an surface onto the renderer immediately. Args: surface: The surface to draw. pos: The position to draw the surface at. Defaults to (0, 0). camera: The camera to use. Defaults to None. """ if not surface.uptodate: surface._regen() if camera is not None: pos = camera.transform(pos) scale = camera.zoom * surface.scale else: scale = surface.scale Display._update(surface._tx, surface.width, surface.height, pos, scale, surface.rotation)
[docs] @classmethod def clear_cache(cls): """ Clears the draw cache. Generally, you shouldn't need to call this method, but it can help free up memory if you're running low; the true best way to avoid this though is to rely on surfaces for shapes that change/recolor often, and call the draw surface method directly instead of the draw shape methods. """ cls._pt_surfs.clear() cls._line_surfs.clear() cls._rect_surfs.clear() cls._circle_surfs.clear() cls._poly_surfs.clear()
@classmethod def _cache_size(cls): return len(cls._pt_surfs) + len(cls._line_surfs) + len(cls._rect_surfs) \ + len(cls._circle_surfs) + len(cls._poly_surfs)