"""
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.
"""
import random
from typing import Any
from ..bruhutil.bruhtypes import EffectType
from .base_renderer import BaseRenderer
[docs]
class FocusRenderer(BaseRenderer):
"""
A Renderer that takes an image and randomly spreads the characters around the screen.
The characters are then pulled to the middle of the screen
"""
[docs]
def __init__(
self,
screen,
img: list[str],
frames: int = 100,
frame_time: float = 0.1,
effect_type: EffectType = "static",
background: str = " ",
transparent: bool = False,
collision: bool = False,
start_frame: int = 0,
reverse: bool = False,
start_reverse: int = None,
loop: bool = True,
settings: Any = None,
preset: str | None = None,
):
super().__init__(
screen,
frames,
frame_time,
effect_type,
background,
transparent,
collision,
settings=settings,
preset=preset,
)
self.img = img
self.start_frame = start_frame
self.reverse = reverse
self.start_reverse = start_reverse
if self.start_reverse:
self.frame_gap = start_reverse - start_frame
self.loop = True if loop and reverse else False
self.loops = 1
if self.reverse and self.start_reverse is None:
raise ValueError(
"if reverse is enabled, a start_reverse frame must be provided"
)
if self.reverse and start_reverse < self.start_frame:
raise ValueError(
f"start_reverse ({self.start_reverse}) cannot be less than start_frame ({self.start_frame})"
)
if self.img:
self._set_img_attributes()
[docs]
def _set_img_attributes(self):
"""
Sets attributes for the image, such as its height and width,
and initializes boards to track character positions.
"""
self.img_height = len(self.img)
self.img_width = len(self.img[0])
self.img_y_start = (self.height - len(self.img)) // 2
self.img_x_start = (self.width - len(self.img[0])) // 2
self.current_img_x = self.img_x_start
self.current_img_y = self.img_y_start
# Initialize start board with characters at random positions
self.start_board = [
[
[
random.randint(0, self.width - 1),
random.randint(0, self.height - 1),
self.img[y][x],
(x, y),
]
for x in range(self.img_width)
]
for y in range(self.img_height)
]
# Initialize current board with characters at start positions
self.current_board = [
[
[
self.start_board[y][x][0],
self.start_board[y][x][1],
self.img[y][x],
(x, y),
]
for x in range(self.img_width)
]
for y in range(self.img_height)
]
# Initialize end board with characters at target positions
self.end_board = [
[
[self.img_x_start + x, self.img_y_start + y, self.img[y][x], (x, y)]
for x in range(self.img_width)
]
for y in range(self.img_height)
]
# Initialize direction board to track character movement
self.direction_board = [
[
[
(
-1
if (self.end_board[y][x][0] - self.current_board[y][x][0]) < 0
else 1
),
(
-1
if (self.end_board[y][x][1] - self.current_board[y][x][1]) < 0
else 1
),
]
for x in range(self.img_width)
]
for y in range(self.img_height)
]
[docs]
def update_reverse(self, reverse: bool, start_reverse: int) -> None:
"""
Updates the state of reverse and start_reverse attributes.
Args:
reverse (bool): Whether to enable or disable reverse.
start_reverse (int): The frame number at which to start the reverse.
Raises:
Exception: If reverse is enabled but start_reverse is not provided,
or if start_reverse is less than the current start_frame.
"""
self.reverse = reverse
self.start_reverse = start_reverse
if start_reverse < self.start_frame:
raise ValueError(
f"start_reverse ({self.start_reverse}) cannot be less than start_frame ({self.start_frame})"
)
[docs]
def update_start_frame(self, frame_number):
"""
Updates the state of the start frame attribute.
Args:
frame_number (int): The new start frame number.
Returns:
None
"""
self.start_frame = frame_number
[docs]
def solved(self, end_state: str) -> bool:
"""
Returns True if the current board matches the target state ("end" or "start").
Raises:
ValueError: If end_state is not "end" or "start".
"""
targets = {"end": self.end_board, "start": self.start_board}
if end_state not in targets:
raise ValueError(
f"unknown solved board state for FocusRenderer: {end_state}"
)
target = targets[end_state]
return all(
self.current_board[y][x][0] == target[y][x][0]
and self.current_board[y][x][1] == target[y][x][1]
for y in range(len(self.current_board))
for x in range(len(self.current_board[y]))
)
[docs]
def render_img_frame(self, frame_number):
"""
Renders the image on the screen based on the current frame number.
Args:
frame_number (int): The current frame number.
Returns:
None
"""
if self.reverse and frame_number >= self.start_reverse:
if not self.solved(end_state="start"):
for y in range(len(self.current_board)):
for x in range(len(self.current_board[y])):
x_check, y_check = False, False
if self.current_board[y][x][0] != self.start_board[y][x][0]:
self.current_board[y][x][0] -= self.direction_board[y][x][0]
else:
x_check = True
if self.current_board[y][x][1] != self.start_board[y][x][1]:
self.current_board[y][x][1] -= self.direction_board[y][x][1]
else:
y_check = True
if x_check and y_check:
self.current_board[y][x][2] = None
self.image_buffer.clear_buffer(val=None)
for row in self.current_board:
for value in row:
self.image_buffer.put_char(
value[0], value[1], value[2], transparent=self.transparent
)
else:
if self.loop:
self.loops += 1
self.start_frame = self.start_reverse + self.frame_gap
self.start_reverse = self.start_frame + self.frame_gap
self._set_img_attributes()
self.image_buffer.clear_buffer(val=None)
elif frame_number >= self.start_frame:
if not self.solved(end_state="end"):
for y in range(len(self.current_board)):
for x in range(len(self.current_board[y])):
if self.current_board[y][x][0] != self.end_board[y][x][0]:
self.current_board[y][x][0] += self.direction_board[y][x][0]
if self.current_board[y][x][1] != self.end_board[y][x][1]:
self.current_board[y][x][1] += self.direction_board[y][x][1]
self.image_buffer.clear_buffer(val=None)
for row in self.current_board:
for value in row:
self.image_buffer.put_char(
value[0], value[1], value[2], transparent=self.transparent
)