import os import datetime from time import sleep # debug class Color: cache_closest_printable_color = 0 def __init__(self, r: float = 0.0, g: float = 0.0, b: float = 0.0): self.set(r, g, b) self.update() def set(self, r, g, b): self.r, self.g, self.b = r, g, b def update(self): self._r, self._g, self._b = self.r, self.g, self.b def hasChange(self) -> bool: return any(x != y for x, y in {self._r: self.r, self._g: self.g, self._b: self.b}.items()) def distance(self, to) -> float: dr = self.r - to.r dg = self.g - to.g db = self.b - to.b return (dr * dr + dg * dg + db * db) ** .5 def getClosestPrintableColor(self) -> int: if not self.hasChange(): return self.cache_closest_printable_color closest_distance = float("inf") closest_value = 0 for k, v in COLORS.items(): distance = self.distance(k) if closest_distance > distance: closest_distance = distance closest_value = v self.cache_closest_printable_color = closest_value self.update() return closest_value class Pixel: char = " " text_color = Color() background_color = Color() cached_text_color_value = -1 cached_background_color_value = -1 def __str__(self) -> str: color = "" text_color = self.text_color.getClosestPrintableColor() if text_color != self.cached_text_color_value: color += COLOR_FORMAT % (text_color + COLOR_TEXT) self.cached_text_color_value = text_color background_color = self.background_color.getClosestPrintableColor() if background_color != self.cached_background_color_value: color += COLOR_FORMAT % (background_color + COLOR_BACKGROUND) self.cached_background_color_value = background_color return color + self.char def reset(self): self.cached_text_color_value = -1 self.cached_background_color_value = -1 class Screen: def __init__(self, width: int, height: int, std_handle: int = 0, render_funcs = []): self.width = width self.height = height self.std_handle = std_handle self.render_funcs = render_funcs self.time_start = datetime.datetime.now() self.buffer = "" self.dynamic = self.height < 0 and self.width < 0 def get_time(self): return (datetime.datetime.now() - self.time_start).total_seconds() def write(self, string): self.buffer += string def flush(self): print(self.buffer, end="", flush=True) self.buffer = "" def mainloop(self): pixel = Pixel() while True: width, height = os.get_terminal_size(self.std_handle) if self.dynamic: self.width, self.height = width, height if width < self.width or height < self.height: Screen.clear() print("Unable to print the screen, a terminal with %dx%d (and above) chars are required to continue" % (self.width, self.height)) while width < self.width or height < self.height: width, height = os.get_terminal_size(self.std_handle) time = self.get_time() resolution = (self.width, self.height) for y in range(self.height): for x in range(self.width): for render_func in self.render_funcs: render_func(pixel, resolution, (x, y), time) self.write(str(pixel)) pixel.reset() self.write("\x1b[0m\n") # newline #Screen.clear() #self.flush() self.new_flush() #sleep(.01) def new_flush(self): self.buffer = "\x1b[0;0H" + self.buffer self.flush() @staticmethod def clear(): print("\x1b[2J", end="", flush=True) COLOR_RESET = 0 COLOR_TEXT = 30 COLOR_BACKGROUND = 40 COLOR_FORMAT = "\x1b[%dm" COLORS = { Color(0.0, 0.0, 0.0): 0, Color(1.0, 0.0, 0.0): 1, Color(0.0, 1.0, 0.0): 2, Color(1.0, 1.0, 0.0): 3, Color(0.0, 0.0, 1.0): 4, Color(1.0, 0.0, 1.0): 5, Color(0.0, 1.0, 1.0): 6, Color(1.0, 1.0, 1.0): 7 }