Source code for gale.stencil

"""
gale.stencil: cut an arbitrary shape (drawn however you like: circles,
polygons, another sprite, text...) out of a surface's alpha channel.

Pygame has no GPU stencil buffer to piggyback on, so this masks on the
CPU instead: draw the shape you want visible into a Stencil's own
mask surface, then apply() it to any other surface — every pixel
outside the shape has its alpha zeroed out (or, with invert=True,
every pixel inside it does). That's the classic building block for
top-down games: a circular torch/vision reveal over a dark overlay, a
minimap cropped to a circle, an item silhouette showing only through
a doorway, and so on.

Author: Alejandro Mujica (aledrums@gmail.com)
"""

from typing import Callable, Tuple

import pygame

# The convention this module relies on: draw stencil shapes as
# opaque white (any alpha) onto the mask surface. BLEND_RGBA_MULT
# then leaves a masked-in pixel's own color/alpha untouched (channel
# * 255 / 255) and zeroes out a masked-out one (channel * 0 / 255).
_OPAQUE_WHITE: pygame.Color = pygame.Color(255, 255, 255, 255)
_TRANSPARENT: pygame.Color = pygame.Color(0, 0, 0, 0)

DrawFunction = Callable[[pygame.Surface], None]


[docs] class Stencil: """ Usage example (a circular torch reveal over a dark overlay): stencil = Stencil((width, height)) stencil.draw(lambda mask: pygame.draw.circle(mask, "white", light_pos, radius)) overlay = pygame.Surface((width, height), pygame.SRCALPHA) overlay.fill((0, 0, 0, 230)) stencil.apply(overlay, invert=True) # punch the circle out of the dark overlay screen.blit(overlay, (0, 0)) The shape is redrawn every frame you want it to move/change: call clear() first, then draw() again (or build a fresh Stencil if you prefer). A single Stencil can be applied to as many surfaces as you like, with or without invert, once its shape is drawn. """ def __init__(self, size: Tuple[int, int]) -> None: """ :param size: The (width, height) of the surfaces this stencil will be applied to. """ self.size: Tuple[int, int] = size self._mask: pygame.Surface = pygame.Surface(size, pygame.SRCALPHA) self._mask.fill(_TRANSPARENT)
[docs] def clear(self) -> None: """ Erase whatever shape was previously drawn, so this Stencil can be reused for a new one. """ self._mask.fill(_TRANSPARENT)
[docs] def draw(self, draw_function: DrawFunction) -> None: """ :param draw_function: Called with this stencil's internal mask surface — draw the shape that should be "visible" onto it (e.g. with pygame.draw.circle/polygon or a blit), in opaque white. """ draw_function(self._mask)
[docs] def apply(self, surface: pygame.Surface, invert: bool = False) -> None: """ Zero out the alpha of every pixel of surface that falls outside the shape previously passed to draw() (or, with invert=True, every pixel inside it). surface must have per-pixel alpha (created with pygame.SRCALPHA, or via convert_alpha()) for this to have a visible effect once it's later blit somewhere else. :param surface: The surface to mask, in place. Must be the same size this Stencil was created with. :param invert: Whether to keep the outside of the shape instead of the inside. The default value is False. """ mask = self._mask if invert: mask = pygame.Surface(self.size, pygame.SRCALPHA) mask.fill(_OPAQUE_WHITE) mask.blit(self._mask, (0, 0), special_flags=pygame.BLEND_RGBA_SUB) surface.blit(mask, (0, 0), special_flags=pygame.BLEND_RGBA_MULT)