"""
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