diff options
Diffstat (limited to 'display_animation.py')
-rw-r--r-- | display_animation.py | 167 |
1 files changed, 167 insertions, 0 deletions
diff --git a/display_animation.py b/display_animation.py new file mode 100644 index 0000000..b9050d5 --- /dev/null +++ b/display_animation.py @@ -0,0 +1,167 @@ +import argparse +import json +import math +import time +import tkinter as tk + +import numpy as np +import pygame + +from utils import rgb_to_hex + + +def parse_args(): + return { + "cell_size": 8, # in pixels + "num_cells_w": 100, # TODO: w/h or r/c? + "num_cells_h": 100, + "colors_path": "configs/colors_64_v0.json", + "corners_path": "configs/corners_hollow4x4_v0.json", + "frame_delay": 1000 // 20, # time b/w frames, in millis + } + + +def seq_to_frames(bin_seq: np.ndarray, args: dict) -> np.ndarray: # TODO: Namespace + """ + Converts a binary sequence to an array of frames. + TODO: doc, expecting size of exactly one frame in the future? + + np.packbits seems relevant but limited to 8 bits + + Args: + bin_seq: a 1D array of binary values. + args: config parameters + + Returns: + array of shape (num_frames, args["num_cells_h"], args["num_cells_w"]), where each element is + a hex string for the color of that cell + """ + bin_seq = bin_seq.copy() # so that we don't mutate bin_seq + + with open(args["colors_path"], "r") as f: + colors = json.load(f) + + with open(args["corners_path"], "r") as f: + corners = json.load(f) + + assert len(corners["corner_colors"]) == 4, "Hardcoded for 4 corners" + corner_width = corners["corner_width"] + corner_height = corners["corner_height"] + + num_colors = len(colors) + bits_per_cell = int(math.log2(num_colors)) + assert 2**bits_per_cell == num_colors, "Assumed the number of colors is a power of 2." + + # bits_per_frame = bits_per_cell * args["num_cells_w"] * args["num_cells_h"] + # num_frames = int(math.ceil(len(bin_seq) / bits_per_frame)) + # bin_seq.resize(bits_per_frame * num_frames) + # # frames_bits = bin_seq.reshape(num_frames, args["num_cells_h"], args["num_cells_w"], bits_per_cell) + # + # pows = 2 ** np.arange(bits_per_cell) + # frames_vals = (frames_bits * pows).sum(axis=-1) # low to high bit order + + cells_per_frame = args["num_cells_w"] * args["num_cells_h"] - 4 * corner_width * corner_height + bits_per_frame = bits_per_cell * cells_per_frame + num_frames = int(math.ceil(len(bin_seq) / bits_per_frame)) + bin_seq.resize(bits_per_frame * num_frames) + frames_bits = bin_seq.reshape(num_frames, cells_per_frame, bits_per_cell) + pows = 2 ** np.arange(bits_per_cell) + frames_vals = (frames_bits * pows).sum(axis=-1) # low to high bit order + + # Efficiently map frame_vals to the corresponding hex colors (https://stackoverflow.com/a/55950051) + color_mapping_arr = np.empty(num_colors, dtype="<U7") + for i in range(num_colors): + color_mapping_arr[i] = rgb_to_hex(colors[str(i)]) # JSON keys stored as str + + frames_colors = color_mapping_arr[frames_vals] + + num_top_cells = (args["num_cells_w"] - 2 * corner_width) * corner_height # TODO: explain + top_cells = frames_colors[:, :num_top_cells].reshape(num_frames, corner_height, -1) + center_cells = frames_colors[:, num_top_cells:-num_top_cells].reshape(num_frames, -1, args["num_cells_w"]) + bottom_cells = frames_colors[:, -num_top_cells:].reshape(num_frames, corner_height, -1) + + corners_numpy_dict = {key: np.broadcast_to(val, (num_frames, corner_height, corner_width)) + for key, val in corners["corner_colors"].items()} + top_rows = np.concatenate([corners_numpy_dict["0"], top_cells, corners_numpy_dict["1"]], axis=2) + bottom_rows = np.concatenate([corners_numpy_dict["2"], bottom_cells, corners_numpy_dict["3"]], axis=2) + + return np.concatenate([top_rows, center_cells, bottom_rows], axis=1) + + +class AnimatedFrames: + def __init__(self, frames: np.ndarray, args: dict): + self.frames = frames + self.args = args + + + def display_frame(self): + func_start_time = time.time_ns() + for inds_tuple, color_hex in np.ndenumerate(self.frames[self.frame_ind % len(self.frames)]): + self.canvas.itemconfigure(self.inds_to_id[inds_tuple], fill=color_hex) + + self.canvas.itemconfigure(self._debug_text_id, text=self.frame_ind) + self.frame_ind += 1 + + adjusted_delay = round((self.start_time + self.args["frame_delay"] * int(1e6) + * self.frame_ind - func_start_time) / 1e6) + assert adjusted_delay > 1, adjusted_delay # o/w we lagged too far behind, assuming 1 ms for this instruction + self.canvas.after(adjusted_delay, self.display_frame) # TODO: check delay is exact + # TODO: set self time and just sleep until then + + def animate(self): + epilepsy_warning_id = self.canvas.create_text(self.width_pixels / 2, self.height_pixels / 2, + text="Warning: Epilepsy") + + def delete_and_animate(): + self.canvas.delete(epilepsy_warning_id) + self.start_time = time.time_ns() + self.display_frame() + + self.canvas.after(5000, delete_and_animate) + # self.display_frame() + self.root.mainloop() + + +if __name__ == "__main__": + args = parse_args() + + rand_bin_seq = np.random.randint(2, size=1000000) + frames = seq_to_frames(rand_bin_seq, args) + + width_pixels = args["cell_size"] * args["num_cells_w"] + height_pixels = args["cell_size"] * args["num_cells_h"] + + pygame.init() + clock = pygame.time.Clock() + screen = pygame.display.set_mode((width_pixels, height_pixels)) + font = pygame.font.SysFont(None, 25) + pygame.event.get() + + text = font.render("<Epilepsy Warning>", True, "white") + text_rect = text.get_rect(center=(width_pixels / 2, height_pixels / 2)) + screen.blit(text, text_rect) + pygame.display.update() + clock.tick(0.2) + frame_ind = 0 + running = True + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + for (i, j), color_hex in np.ndenumerate(frames[frame_ind % len(frames)]): + rect = pygame.Rect(i * args["cell_size"], j * args["cell_size"], args["cell_size"], args["cell_size"]) + pygame.draw.rect(screen, color_hex, rect) + + text = font.render(str(frame_ind), True, "black") + text_rect = text.get_rect(center=((args["num_cells_w"] - 2) * args["cell_size"], (args["num_cells_h"] - 2) * args["cell_size"])) + screen.blit(text, text_rect) + frame_ind += 1 + pygame.display.update() + # clock.tick(15) + clock.tick_busy_loop(10) # https://gamedev.stackexchange.com/a/102831 + if frame_ind % 30 == 0: + print(f"{clock.get_fps()=}") + + # https://stackoverflow.com/questions/28405222/syncing-image-display-with-screen-refresh-rate? + pygame.quit() |