| """Braille-character canvas for high-resolution terminal graphics. |
| |
| Each terminal cell maps to a 2x4 dot grid using Unicode braille characters |
| (U+2800βU+28FF), giving 2Γ horizontal and 4Γ vertical resolution. |
| """ |
|
|
| |
| |
| |
| |
| _DOT_MAP = ( |
| (0x01, 0x08), |
| (0x02, 0x10), |
| (0x04, 0x20), |
| (0x40, 0x80), |
| ) |
|
|
|
|
| class BrailleCanvas: |
| """A pixel canvas that renders to braille characters.""" |
|
|
| def __init__(self, term_width: int, term_height: int): |
| self.term_width = term_width |
| self.term_height = term_height |
| self.pixel_width = term_width * 2 |
| self.pixel_height = term_height * 4 |
| self._buf = bytearray(term_width * term_height) |
|
|
| def clear(self) -> None: |
| for i in range(len(self._buf)): |
| self._buf[i] = 0 |
|
|
| def set_pixel(self, x: int, y: int) -> None: |
| if 0 <= x < self.pixel_width and 0 <= y < self.pixel_height: |
| cx, rx = divmod(x, 2) |
| cy, ry = divmod(y, 4) |
| self._buf[cy * self.term_width + cx] |= _DOT_MAP[ry][rx] |
|
|
| def render(self) -> list[str]: |
| lines = [] |
| for row in range(self.term_height): |
| offset = row * self.term_width |
| line = "".join( |
| chr(0x2800 + self._buf[offset + col]) for col in range(self.term_width) |
| ) |
| lines.append(line) |
| return lines |
|
|
|
|
| |
|
|
| _FONT: dict[str, list[str]] = {} |
|
|
|
|
| def _define_font() -> None: |
| """Define a simple 5Γ7 bitmap font for uppercase ASCII.""" |
| glyphs = { |
| "A": [" ## ", "# #", "# #", "####", "# #", "# #", "# #"], |
| "B": ["### ", "# #", "# #", "### ", "# #", "# #", "### "], |
| "C": [" ## ", "# #", "# ", "# ", "# ", "# #", " ## "], |
| "D": ["### ", "# #", "# #", "# #", "# #", "# #", "### "], |
| "E": ["####", "# ", "# ", "### ", "# ", "# ", "####"], |
| "F": ["####", "# ", "# ", "### ", "# ", "# ", "# "], |
| "G": [" ## ", "# #", "# ", "# ##", "# #", "# #", " ###"], |
| "H": ["# #", "# #", "# #", "####", "# #", "# #", "# #"], |
| "I": ["###", " # ", " # ", " # ", " # ", " # ", "###"], |
| "J": [" ##", " # ", " # ", " # ", " # ", "# # ", " # "], |
| "K": ["# #", "# # ", "## ", "## ", "# # ", "# #", "# #"], |
| "L": ["# ", "# ", "# ", "# ", "# ", "# ", "####"], |
| "M": ["# #", "## ##", "# # #", "# # #", "# #", "# #", "# #"], |
| "N": ["# #", "## #", "## #", "# ##", "# ##", "# #", "# #"], |
| "O": [" ## ", "# #", "# #", "# #", "# #", "# #", " ## "], |
| "P": ["### ", "# #", "# #", "### ", "# ", "# ", "# "], |
| "Q": [" ## ", "# #", "# #", "# #", "# ##", "# #", " ## "], |
| "R": ["### ", "# #", "# #", "### ", "# # ", "# #", "# #"], |
| "S": [" ## ", "# #", "# ", " ## ", " #", "# #", " ## "], |
| "T": ["#####", " # ", " # ", " # ", " # ", " # ", " # "], |
| "U": ["# #", "# #", "# #", "# #", "# #", "# #", " ## "], |
| "V": ["# #", "# #", "# #", " # # ", " # # ", " # ", " # "], |
| "W": ["# #", "# #", "# #", "# # #", "# # #", "## ##", "# #"], |
| "X": ["# #", "# #", " ## ", " ## ", " ## ", "# #", "# #"], |
| "Y": ["# #", "# #", " # # ", " # ", " # ", " # ", " # "], |
| "Z": ["####", " #", " # ", " # ", "# ", "# ", "####"], |
| " ": [" ", " ", " ", " ", " ", " ", " "], |
| "0": [" ## ", "# #", "# #", "# #", "# #", "# #", " ## "], |
| "1": [" # ", "## ", " # ", " # ", " # ", " # ", "###"], |
| "2": [" ## ", "# #", " #", " # ", " # ", "# ", "####"], |
| "3": [" ## ", "# #", " #", " ## ", " #", "# #", " ## "], |
| "4": ["# #", "# #", "# #", "####", " #", " #", " #"], |
| "5": ["####", "# ", "### ", " #", " #", "# #", " ## "], |
| "6": [" ## ", "# ", "### ", "# #", "# #", "# #", " ## "], |
| "7": ["####", " #", " # ", " # ", " # ", " # ", " # "], |
| "8": [" ## ", "# #", "# #", " ## ", "# #", "# #", " ## "], |
| "9": [" ## ", "# #", "# #", " ###", " #", " #", " ## "], |
| } |
| _FONT.update(glyphs) |
|
|
|
|
| _define_font() |
|
|
|
|
| def text_to_pixels(text: str, scale: int = 1) -> list[tuple[int, int]]: |
| """Convert text string to a list of (x, y) pixel positions using bitmap font.""" |
| pixels = [] |
| cursor_x = 0 |
| for ch in text.upper(): |
| glyph = _FONT.get(ch) |
| if glyph is None: |
| cursor_x += 4 * scale |
| continue |
| for row_idx, row in enumerate(glyph): |
| for col_idx, cell in enumerate(row): |
| if cell == "#": |
| for sy in range(scale): |
| for sx in range(scale): |
| pixels.append( |
| (cursor_x + col_idx * scale + sx, row_idx * scale + sy) |
| ) |
| glyph_width = max(len(r) for r in glyph) |
| cursor_x += (glyph_width + 1) * scale |
| return pixels |
|
|