Source code for bruhanimate.bruhrenderer.pan_renderer

"""
Copyright 2023 Ethan Christensen

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

from typing import Any

from ..bruhutil import Screen
from ..bruhutil.bruherrors import InvalidPanRendererDirectionError
from ..bruhutil.bruhtypes import (
    EffectType,
    PanRendererDirection,
    valid_pan_renderer_directions,
)
from .base_renderer import BaseRenderer


[docs] class PanRenderer(BaseRenderer): """ A renderer to pan an image across the screen. Updates the image_buffer only. """
[docs] def __init__( self, screen: Screen, img: list[str], frames: int = 100, frame_time: float = 0.1, effect_type: EffectType = "static", background: str = " ", transparent: bool = False, collision: bool = False, direction: PanRendererDirection = "horizontal", shift_rate: int = 1, loop: bool = False, settings: Any = None, preset: str | None = None, ) -> None: super().__init__( screen, frames, frame_time, effect_type, background, transparent, collision, settings=settings, preset=preset, ) self.direction = self.validate_direction(direction) self.img = img self.shift_rate = max(1, int(shift_rate)) self.loop = loop if self.img: self._set_img_attributes()
[docs] def validate_direction( self, direction: PanRendererDirection ) -> PanRendererDirection: """ Validates the given direction to ensure it is one of the valid pan renderer directions. Args: direction (PanRendererDirection): The direction to be validated. Returns: PanRendererDirection: The validated direction. Raises: InvalidPanRendererDirectionError: If the given direction is not a valid pan renderer direction. """ if direction not in valid_pan_renderer_directions: raise InvalidPanRendererDirectionError( f"Invalid direction for PanRenderer. Please choose from {valid_pan_renderer_directions}" ) return direction
[docs] def _set_img_attributes(self) -> None: """ Sets attributes related to the image, including whether to render it, its height and width, and its initial position on the screen. """ self.render_image = True self.img_height = len(self.img) self.img_width = len(self.img[0]) self.img_back = -self.img_width - 1 self.img_front = -1 self.img_top = (self.height - self.img_height) // 2 self.img_bottom = ((self.height - self.img_height) // 2) + self.img_height self.current_img_x = self.img_back self.current_img_y = self.img_top
@property def img_size(self) -> tuple[int, int]: """ Returns the (height, width) of the image, or (0, 0) if the image is empty. """ return (len(self.img), len(self.img[0])) if self.img else (0, 0)
[docs] def render_img_frame(self, frame_number: int) -> None: """ Renders a single frame of the image. Args: frame_number: The current frame number. """ if not self.loop and self.img_back > self.width + 1: return if self.direction == "horizontal": self.render_horizontal_frame(frame_number=frame_number) elif self.direction == "vertical": self.render_vertical_frame(frame_number=frame_number)
[docs] def _set_padding(self, padding_vals: tuple[int, int]) -> None: """ Sets the image's padding based on the provided values. Args: padding_vals (tuple[int, int]): A tuple containing two integers representing the left/right and top/bottom padding. Returns: None Raises: ValueError: If the provided padding value is invalid or if no image has been set. """ if not self.img or len(padding_vals) != 2: return left_right, top_bottom = padding_vals self.padding = (left_right, top_bottom) # Create a new image with the desired padding self.img = ( [" " * self.img_width for _ in range(top_bottom)] + [(" " * left_right) + line + (" " * left_right) for line in self.img] + [" " * self.img_width for _ in range(top_bottom)] ) # Update the image attributes to reflect the new padding self._set_img_attributes()
[docs] def render_horizontal_frame(self, frame_number): """ Renders a horizontal frame of the image. Args: frame_number: The current frame number. """ if self.shift_rate > 0: # Calculate the number of frames to render num_frames = (self.img_width // self.shift_rate) + 1 # Stop only when not looping and we've exhausted the frame sequence if not self.loop and frame_number >= num_frames: return # Update the image position based on the shift rate and current frame number new_img_back = -self.img_width - (frame_number * self.shift_rate) new_img_front = frame_number * self.shift_rate # Render each row of the image at its new position for y in range(self.height): for x in range(self.width): if ( x >= new_img_back and x < new_img_front and y >= self.img_top and y < self.img_bottom ): # Check if we're within the bounds of the image if self.img_height > 0 and self.img_width > 0: img_row = self.img[y - self.img_top] # Check if each pixel in the row is within the bounds of the image for j, pixel in enumerate(img_row): if x - new_img_back >= j: # Put the pixel into the screen buffer at its new position if self.transparent: if ( y == self.img_top + (self.img_height // 2) and x - new_img_back == 0 ) or self.img[y - self.img_top][j] != " ": self.image_buffer.put_char(x, y, pixel) else: self.image_buffer.put_char(x, y, pixel) # Update the image buffer to reflect the updated position of the image self.image_buffer.shift(self.shift_rate) return
[docs] def render_vertical_frame(self, frame_number): """ Renders a vertical frame of the image. Args: frame_number: The current frame number. """ if self.shift_rate > 0: # Calculate the number of frames to render num_frames = (self.img_height // self.shift_rate) + 1 # Check if we're in loop mode or have reached the end of the image if not self.loop and frame_number >= num_frames: return # Update the image position based on the shift rate and current frame number new_img_top = -self.img_height - (frame_number * self.shift_rate) new_img_bottom = frame_number * self.shift_rate # Render each column of the image at its new position for x in range(self.width): for y in range(self.height): if ( y >= new_img_top and y < new_img_bottom and x >= self.img_back and x < self.img_front ): # Check if we're within the bounds of the image if self.img_height > 0 and self.img_width > 0: column_index = x - self.img_back # Check if each pixel in the column is within the bounds of the image img_column = [ self.img[j][column_index] for j in range(self.img_height) ] for i, pixel in enumerate(img_column): if y - new_img_top >= i: # Put the pixel into the screen buffer at its new position if self.transparent: if self.img[i][column_index] != " ": self.image_buffer.put_char(x, y, pixel) else: self.image_buffer.put_char(x, y, pixel) # Update the image buffer to reflect the updated position of the image self.image_buffer.shift_vertical(self.shift_rate) return