Source code for miniworldmaker.appearances.appearance

import asyncio
import inspect
from collections import defaultdict
import nest_asyncio
import pygame
from miniworldmaker.board_positions import board_position


class MetaAppearance(type):
    def __call__(cls, *args, **kwargs):
        try:
            instance = super().__call__(*args, **kwargs)
        except TypeError:
            raise TypeError(
                "Wrong number of arguments for {0}-constructor. See method-signature: {0}{1}".format(cls.__name__,
                                                                                                     inspect.signature(
                                                                                                         cls.__init__)))
        instance.after_init()
        return instance


[docs]class Appearance(metaclass=MetaAppearance): """ Base class of token costumes and board backgrounds Die Klasse enthält alle Methoden und Attribute um Bilder der Objekte anzuzeigen, zu animieren, Text auf den Bildern zu rendern oder Overlays anzuzeigen. """ _images_dict = {} # dict with key: image_path, value: loaded image def __init__(self): self.dirty = 0 self.blit_images = [] # Images which are blitted on the background self.parent = None self.images_list = [] # Original images self._image_index = 0 # current_image index (for animations) self.image_paths = [] # list with all images # properties self.raw_image = pygame.Surface((1, 1)) # size set in image()-method self._image = pygame.Surface((1, 1)) # size set in image()-method self.cached_image = pygame.Surface((1, 1)) self.call_image_actions = {} self.animation_speed = 100 #: The animation speed for animations self._is_animated = False self._is_flipped = False self._is_textured = False self._is_upscaled = False self._is_scaled = False self._is_rotatable = False self._orientation = False self._text = "" self._coloring = None # Color for colorize operation self.draw_shapes = [] # "Action name", image_action_method, "Attribute", enabled) self.image_actions_pipeline = [ ("orientation", self.image_action_set_orientation, "orientation", False), ("draw_shapes", self.image_action_draw_shapes, "draw_shapes", False), ("texture", self.image_action_texture, "is_textured", False), ("scale", self.image_action_scale, "is_scaled", False), ("upscale", self.image_action_upscale, "is_upscaled", False), ("write_text", self.image_action_write_text, "text", False), ("flip", self.image_action_flip, "is_flipped", False), ("coloring", self.image_action_coloring, "coloring", False), ("rotate", self.image_action_rotate, "is_rotatable", False), ] self.fill_color = (0, 0, 255, 255) #: background_color if actor has no background image self.color = (255, 255, 255, 255) #: color for overlays self._font_size = 0 #: font_size if token-text != "" self.text_position = (0, 0) #: Position of text relative to the top-left pixel of token self.font_path = None #: Path to font-file self.dirty = 1 self.reload_actions = defaultdict() self.surface_loaded = False self.last_image = None self.cached_images = defaultdict()
[docs] @staticmethod def load_image(path): """ Loads an image from an path. Args: path: The path to image Returns: The image loaded """ try: import os canonicalized_path = os.path.join(os.path.curdir, path) canonicalized_path = str(path).replace('/', os.sep).replace('\\', os.sep) if canonicalized_path in Appearance._images_dict.keys(): # load image from img_dict _image = Appearance._images_dict[canonicalized_path] else: try: _image = pygame.image.load(canonicalized_path).convert_alpha() Appearance._images_dict[canonicalized_path] = _image except pygame.error: raise FileExistsError("File '{0}' does not exist. Check your path to the image.".format(path)) return _image except FileExistsError: raise FileExistsError("File '{0}' does not exist. Check your path to the image.".format(path))
def after_init(self): self._reload_all() self._update() def _reload_all(self): for img_action in self.image_actions_pipeline: self.reload_actions[img_action[0]] = True self._update() @property def font_size(self): return self._font_size @font_size.setter def font_size(self, value): self._font_size = value self.call_action("write_text") @property def is_textured(self): """ bool: If True, the image is tiled over the background. Examples: Defines a textured board >>> class MyBoard(TiledBoard): >>> def on_setup(self): >>> self.add_image(path="images/stone.png") >>> self.background.is_textured = True >>> self.background.is_scaled_to_tile = True >>> self.player = Player(position=(3, 4)) """ return self._is_textured @is_textured.setter def is_textured(self, value): self._is_textured = value self.call_action("texture") @property def is_upscaled(self): """bool: If True, the image will be upscaled remaining aspect-ratio. """ return self._is_upscaled @is_upscaled.setter def is_upscaled(self, value): self._is_upscaled = value self.call_action("upscale") @property def is_rotatable(self): """bool: If True, the image will be rotated by parent direction""" return self._is_rotatable @is_rotatable.setter def is_rotatable(self, value): self._is_rotatable = value self.dirty = 1 @property def orientation(self): """bool: If True, the image will be rotated by parent orientation before it is rotated. This should be used, if image is not pointing to right direction""" return self._orientation @orientation.setter def orientation(self, value): self._orientation = value if self.orientation != self.parent._orientation: self.parent.orientation = self._orientation self.call_action("orientation") @property def is_flipped(self): """bool: Flips the token by 180° degrees. This can be used e.g. for bouncing actor at border""" return self._is_flipped @is_flipped.setter def is_flipped(self, value): self._is_flipped = value self.call_action("flip") @property def is_scaled(self): """ bool: Scales the actor to parent-size withour remaining aspect-ratio.""" return self._is_scaled @is_scaled.setter def is_scaled(self, value): self._is_scaled = value self.call_action("scale") @property def coloring(self): """ Defines a colored layer. Coloring is True or false. The color is defined by the attribute appearance.color """ return self._coloring @coloring.setter def coloring(self, value): self._coloring = value self.call_action("coloring") self.dirty = 1 @property def text(self): """ If text!= "" a Text is rendered at the token-position. """ return self._text @text.setter def text(self, value): if value == "": self._text = "" # self.surface_loaded = False self.dirty = 1 else: self._text = value # self.surface_loaded = False self.dirty = 1 self.call_action("write_text") self._reload_all() def fill(self, color): try: self.fill_color = color self.surface_loaded = False self.dirty = 1 except TypeError: self.parent.window.log("ERROR: color should be a 4-tuple (r, g, b, alpha)") raise () def draw_shape_append(self, shape, arguments): self.draw_shapes.append((shape, arguments)) self.call_action("draw_shapes") def draw_shape_set(self, shape, arguments): self.draw_shapes = [(shape, arguments)] self.call_action("draw_shapes")
[docs] def add_image(self, path: str) -> int: """ Adds an image to the appearance Args: path: The path to the image relative to actual directory crop: tuple: x,y,width, height Returns: The index of the added image. """ _image = Appearance.load_image(path) self.images_list.append(_image) self.image_paths.append(path) self._image = self.image self.dirty = 1 self._reload_all() self._update() return len(self.images_list) - 1
[docs] def blit(self, path, position: tuple, size: tuple = (0, 0)): """ Blits an image to the background Args: path: Path to the image position: Top left position size: Size of blitted image Returns: """ _blit_image = Appearance.load_image(path) if size != (0, 0): _blit_image = pygame.transform.scale(_blit_image, size) self.image.blit(_blit_image, position) self.blit_images.append((_blit_image, position, size))
@property def image(self) -> pygame.Surface: return self._image def load_surface(self) -> pygame.Surface: if not self.surface_loaded: image = pygame.Surface(self.parent.size, pygame.SRCALPHA) image.fill(self.fill_color) image.set_alpha(255) self.dirty = 1 self.raw_image = image return self.raw_image def reload_image(self): if self.dirty == 1: if self.images_list and self.images_list[self._image_index]: image = self.images_list[self._image_index] # if there is a image list load image by index else: # no image files - Render raw surface image = self.load_surface() for img_action in self.image_actions_pipeline: # If an image action is to be executed again, # load the last cached image from the pipeline and execute # all subsequent image actions. if self.reload_actions[img_action[0]] is False \ and img_action[0] in self.cached_images.keys() \ and self.cached_images[img_action[0]]: if getattr(self, img_action[2]): if self.parent.size != (0, 0): image = self.cached_images[img_action[0]] # Reload image from cache else: # reload_actions is true if getattr(self, img_action[2]): if self.parent.size != (0, 0): image = img_action[1](image, parent=self.parent) # perform image action self.cached_images[img_action[0]] = image self.parent.dirty = 1 for blit_image in self.blit_images: image.blit(blit_image[0], blit_image[1]) self._image = image self.dirty = 0 for key in self.reload_actions.keys(): self.reload_actions[key] = False return self.image
[docs] async def next_image(self): """ Switches to the next image of the appearance. """ if self.is_animated: if self._image_index < len(self.images_list) - 1: self._image_index = self._image_index + 1 else: self._image_index = 0 self.dirty = 1 self.parent.dirty = 1 self._reload_all()
def set_image(self, value): if 0 <= value < len(self.images_list) - 1: self.image_index = value self.dirty = 1 self.parent.dirty = 1 self._reload_all() self._update() return True else: return False def _update(self): loop = asyncio.get_event_loop() task = loop.create_task(self.update()) nest_asyncio.apply() loop.run_until_complete(task) return 1 async def update(self): pass @property def is_animated(self): """bool: If True, the image will be animated. Depends on appearance.animation_speed """ return self._is_animated @is_animated.setter def is_animated(self, value): self._is_animated = value
[docs] def count_pixels_by_color(self, rect, color, threshold=(0, 0, 0, 0)): """ Counts the number of pixels of a color under the appearance. Args: color: The color threshold: The allowed deviation from the color splitted into r,g,b and alpha values. Returns: The number of matching pixes """ surf = pygame.Surface((rect.width, rect.height)) surf.blit(self._image, (0, 0), rect) return pygame.transform.threshold(dest_surf=None, set_behavior=0, surf=surf, search_color=color, threshold=threshold)
[docs] def color_at(self, position: board_position.BoardPosition) -> tuple: """ Returns the color at a specific position Args: position: The position to search for Returns: The color """ if type(position) == tuple: position = board_position.BoardPosition(position[0], position[1]) if position.is_on_board(): return self._image.get_at(position.to_pixel())
def call_action(self, action): reload = False for img_action in self.image_actions_pipeline: if img_action[0] == action: reload = True # reload image action if reload: self.reload_actions[img_action[0]] = True # reload all actions after image action self.dirty = 1 self.parent.dirty = 1 self._update() def call_actions(self, actions): reload = False for img_action in self.image_actions_pipeline: if img_action[0] in actions: reload = True if reload: self.reload_actions[img_action[0]] = True self.dirty = 1 self.parent.dirty = 1 self._update() return self.image def call_all_actions(self): for img_action in self.image_actions_pipeline: self.reload_actions[img_action[0]] = True self.dirty = 1 self.parent.dirty = 1 self._update() return self.image def image_action_draw_shapes(self, image: pygame.Surface, parent) -> pygame.Surface: for draw_action in self.draw_shapes: draw_action[0](image, *draw_action[1]) return image def image_action_texture(self, image, parent): background = pygame.Surface(parent.size) background.fill((255, 255, 255)) i, j, width, height = 0, 0, 0, 0 while width < parent.width: while height < parent.height: width = i * image.get_width() height = j * image.get_height() j += 1 background.blit(image, (width, height)) j, height = 0, 0 i += 1 return background def image_action_upscale(self, image: pygame.Surface, parent) -> pygame.Surface: if parent.size != 0: scale_factor_x = parent.size[0] / image.get_width() scale_factor_y = parent.size[1] / image.get_height() scale_factor = min(scale_factor_x, scale_factor_y) new_width = int(image.get_width() * scale_factor) new_height = int(image.get_height() * scale_factor) image = pygame.transform.scale(image, (new_width, new_height)) return image def image_action_scale(self, image: pygame.Surface, parent, ) -> pygame.Surface: image = pygame.transform.scale(image, parent.size) return image def image_action_rotate(self, image: pygame.Surface, parent) -> pygame.Surface: if self.parent.direction != 0: return pygame.transform.rotate(image, - (self.parent.direction)) else: return image def image_action_set_orientation(self, image: pygame.Surface, parent) -> pygame.Surface: if self.parent.orientation != 0: return pygame.transform.rotate(image, - self.parent.orientation) else: return image def image_action_flip(self, image: pygame.Surface, parent) -> pygame.Surface: return pygame.transform.flip(image, self.is_flipped, False)
[docs] def image_action_coloring(self, image: pygame.Surface, parent) -> pygame.Surface: """ Create a "colorized" copy of a surface (replaces RGB values with the given color, preserving the per-pixel alphas of original). :param image: Surface to create a colorized copy of :param newColor: RGB color to use (original alpha values are preserved) :return: New colorized Surface instance """ image = image.copy() # zero out RGB values image.fill((0, 0, 0, 255), None, pygame.BLEND_RGBA_MULT) # Fill black # add in new RGB values new_color = self.color[0:3] + (0,) image.fill(new_color, None, pygame.BLEND_RGBA_ADD) # Add color new_color = (255, 255, 255) + (self.color[3],) image.fill(new_color, None, pygame.BLEND_RGBA_MULT) # Multiply transparency return image
@staticmethod def crop_image(image: pygame.Surface, parent, appearance) -> pygame.Surface: cropped_surface = pygame.Surface(parent.size) cropped_surface.fill((255, 255, 255)) cropped_surface.blit(image, (0, 0), (0, 0, parent.size[0], parent.size[1])) return cropped_surface def image_action_write_text(self, image: pygame.Surface, parent) -> pygame.Surface: if self.font_path is None: if self.font_size == 0: font_size = parent.size[1] else: font_size = self.font_size my_font = pygame.font.SysFont("monospace", font_size) else: my_font = pygame.font.Font(self.font_path) label = my_font.render(self.text, 1, self.color) image.blit(label, self.text_position) return image