| 1 | #!/usr/bin/env python3 |
| 2 | """Preview archived Codewhale empty-state portrait art: before vs after. |
| 3 | |
| 4 | This script is historical comparison evidence only. It is not a preview of the |
| 5 | current Codewhale mark: the canonical mark is a raster asset with no approved |
| 6 | ASCII or block-glyph substitute. The historical rows are held below as a fixed record because the live |
| 7 | `underwater.rs` constants were intentionally deleted with the old artwork. |
| 8 | |
| 9 | python3 scripts/preview-whale-art.py # ANSI true colour |
| 10 | python3 scripts/preview-whale-art.py --plain # no escape codes |
| 11 | python3 scripts/preview-whale-art.py --png out.png # if Pillow exists |
| 12 | |
| 13 | Colours are the real palette tokens: Signal Gold #F6C453 body, Signal Current |
| 14 | #48D7FF spout and belly cut, ivory eye, on the whale theme's #03070D surface. |
| 15 | """ |
| 16 | |
| 17 | from __future__ import annotations |
| 18 | |
| 19 | import argparse |
| 20 | |
| 21 | GOLD = (246, 196, 83) # WHALE_HUMAN_RGB — Signal Gold |
| 22 | CYAN = (72, 215, 255) # WHALE_CYAN_RGB — Signal Current |
| 23 | IVORY = (233, 238, 245) # text_body |
| 24 | SAKURA = (247, 168, 200) # uwu accent_primary |
| 25 | SEAFOAM = (79, 209, 197) # whale theme accent_secondary (the old spout) |
| 26 | SURFACE = (3, 7, 13) # whale theme surface_bg |
| 27 | |
| 28 | # The art this branch replaced, kept verbatim so the PR is reviewable. |
| 29 | BEFORE = { |
| 30 | "classic": (" ˚", [ |
| 31 | " ▗▄▄▄▄▄▄▄▄▄▄▄▖ ▚△▞", |
| 32 | " ▐██·███████████▙━━━━▞", |
| 33 | " ▝▀▀▀▀▀▀▀▀▀▀▀▀▘", |
| 34 | ]), |
| 35 | "uwu": (" ˚✦", [ |
| 36 | " ▗▄▄▄▄▄▄▄▄▄▄▄▖ ▚△▞", |
| 37 | "▐█░·░█████████▙▄▄▞", |
| 38 | " ▝▀▀▀▀▀▀▀▀▀▀▀▘", |
| 39 | ]), |
| 40 | } |
| 41 | |
| 42 | # The former Signal Cut revision, kept as a static record. It must not be |
| 43 | # inferred from live source: the runtime portrait constants were deliberately |
| 44 | # removed when the product mark changed. |
| 45 | AFTER = { |
| 46 | "classic": (" ˚", [ |
| 47 | " ▗▄▄▟▄▄▄▄▄▖ ▚△▞", |
| 48 | " ▐█·████████▙▄▄▞", |
| 49 | " ▝▀▀▀▀▀▀▀▀▘", |
| 50 | ]), |
| 51 | "uwu": (" ˚✦", [ |
| 52 | " ▗▄▄▟▄▄▄▄▖ ▚△▞", |
| 53 | " ▐█░·░█████▙▄▄▞", |
| 54 | " ▝▀▀▀▀▀▀▀▘", |
| 55 | ]), |
| 56 | } |
| 57 | |
| 58 | # Same table the TUI narrows through (`glyphs::ascii_fallback`). |
| 59 | ASCII_FALLBACK = { |
| 60 | "━": "-", "▐": "|", "█": "#", "▄": "#", "▟": "#", |
| 61 | "▙": "#", "▀": "#", "▗": ".", "▖": ".", "▝": ".", |
| 62 | "▘": ".", "▚": "\\", "▞": "/", "░": ":", "△": "^", |
| 63 | "·": ".", "˚": "o", "✦": "*", |
| 64 | } |
| 65 | |
| 66 | CURRENT_ROW = 2 # IDLE_WHALE_CURRENT_ROW |
| 67 | |
| 68 | |
| 69 | def ink(variant: str, row: int, ch: str, old: bool = False) -> tuple[int, int, int]: |
| 70 | """The colour the TUI resolves for one glyph, without the caustic sweep.""" |
| 71 | if ch in "·░✦△": |
| 72 | return SAKURA if variant == "uwu" else IVORY |
| 73 | if old: |
| 74 | # main: spout took the theme's secondary accent, the belly was body gold. |
| 75 | return SEAFOAM if row < 0 else GOLD |
| 76 | if row < 0: |
| 77 | return CYAN # spout row |
| 78 | if row == CURRENT_ROW: |
| 79 | return CYAN # belly cut |
| 80 | return GOLD |
| 81 | |
| 82 | |
| 83 | def paint(variant: str, spout: str, body: list[str], colour: bool, old: bool = False) -> list[str]: |
| 84 | out = [] |
| 85 | for row, text in enumerate([spout] + body, start=-1): |
| 86 | if not colour: |
| 87 | out.append(text) |
| 88 | continue |
| 89 | line = f"\x1b[48;2;{SURFACE[0]};{SURFACE[1]};{SURFACE[2]}m" |
| 90 | for ch in text: |
| 91 | r, g, b = ink(variant, row, ch, old) |
| 92 | line += f"\x1b[38;2;{r};{g};{b}m{ch}" |
| 93 | out.append(line + "\x1b[0m") |
| 94 | return out |
| 95 | |
| 96 | |
| 97 | def ascii_rows(spout: str, body: list[str]) -> list[str]: |
| 98 | return ["".join(ASCII_FALLBACK.get(ch, ch) for ch in row) for row in [spout] + body] |
| 99 | |
| 100 | |
| 101 | def block(title: str, rows: list[str]) -> None: |
| 102 | print(f" {title}") |
| 103 | for row in rows: |
| 104 | print(f" {row}") |
| 105 | print() |
| 106 | |
| 107 | |
| 108 | # Block-element geometry as (x0, y0, x1, y1) fractions of one terminal cell. |
| 109 | # Terminals scale these to fill the cell exactly; font rasterisers do not, so |
| 110 | # the PNG draws them itself and keeps the font only for the small marks. |
| 111 | CELL_SHAPES: dict[str, tuple[tuple[float, float, float, float], ...]] = { |
| 112 | "█": ((0, 0, 1, 1),), |
| 113 | "▀": ((0, 0, 1, 0.5),), |
| 114 | "▄": ((0, 0.5, 1, 1),), |
| 115 | "▌": ((0, 0, 0.5, 1),), |
| 116 | "▐": ((0.5, 0, 1, 1),), |
| 117 | "▘": ((0, 0, 0.5, 0.5),), |
| 118 | "▝": ((0.5, 0, 1, 0.5),), |
| 119 | "▖": ((0, 0.5, 0.5, 1),), |
| 120 | "▗": ((0.5, 0.5, 1, 1),), |
| 121 | "▙": ((0, 0, 0.5, 1), (0.5, 0.5, 1, 1)), |
| 122 | "▟": ((0.5, 0, 1, 0.5), (0, 0.5, 1, 1)), |
| 123 | "▚": ((0, 0, 0.5, 0.5), (0.5, 0.5, 1, 1)), |
| 124 | "▞": ((0.5, 0, 1, 0.5), (0, 0.5, 0.5, 1)), |
| 125 | "━": ((0, 0.42, 1, 0.6),), |
| 126 | "·": ((0.34, 0.42, 0.66, 0.6),), |
| 127 | "˚": ((0.3, 0.16, 0.7, 0.42),), |
| 128 | "░": ((0, 0, 1, 1),), |
| 129 | } |
| 130 | # Marks the cell grammar draws as outlines or points rather than fills. |
| 131 | RING = {"˚": (0.3, 0.16, 0.7, 0.44)} |
| 132 | TRIANGLE = {"△": (0.22, 0.2, 0.78, 0.76)} |
| 133 | STAR = {"✦": (0.5, 0.35, 0.26)} |
| 134 | SHADED = {"░"} |
| 135 | |
| 136 | |
| 137 | def render_png(after: dict[str, tuple[str, list[str]]], path: str) -> None: |
| 138 | """A terminal-cell mock of BEFORE vs AFTER, drawn as geometry.""" |
| 139 | try: |
| 140 | from PIL import Image, ImageDraw # noqa: PLC0415 |
| 141 | except ImportError: |
| 142 | print(f"! Pillow is not installed; skipping {path}", file=sys.stderr) |
| 143 | return |
| 144 | |
| 145 | cell_w, cell_h, pad = 18, 34, 30 |
| 146 | panels = [ |
| 147 | ("BEFORE · main", BEFORE["classic"], True), |
| 148 | ("ARCHIVED AFTER · Signal Cut", after["classic"], False), |
| 149 | ] |
| 150 | cols = max(len(row) for _, (spout, body), _ in panels for row in [spout] + body) |
| 151 | width = pad * 2 + cols * cell_w |
| 152 | height = pad + len(panels) * (cell_h * 6 + pad) |
| 153 | img = Image.new("RGB", (width, height), SURFACE) |
| 154 | draw = ImageDraw.Draw(img, "RGBA") |
| 155 | |
| 156 | y = pad |
| 157 | for title, (spout, body), old in panels: |
| 158 | draw.text((pad, y + cell_h // 3), title, fill=(120, 138, 158)) |
| 159 | y += cell_h * 2 |
| 160 | for row, text in enumerate([spout] + body, start=-1): |
| 161 | top = y + (row + 1) * cell_h |
| 162 | for col, ch in enumerate(text): |
| 163 | if ch == " ": |
| 164 | continue |
| 165 | left = pad + col * cell_w |
| 166 | colour = ink("classic", row, ch, old) |
| 167 | cell = lambda x0, y0, x1, y1: ( # noqa: E731 |
| 168 | left + x0 * cell_w, |
| 169 | top + y0 * cell_h, |
| 170 | left + x1 * cell_w - 1, |
| 171 | top + y1 * cell_h - 1, |
| 172 | ) |
| 173 | if ch in RING: |
| 174 | draw.ellipse(cell(*RING[ch]), outline=colour, width=2) |
| 175 | elif ch in TRIANGLE: |
| 176 | x0, y0, x1, y1 = cell(*TRIANGLE[ch]) |
| 177 | draw.polygon([((x0 + x1) / 2, y0), (x1, y1), (x0, y1)], outline=colour, width=2) |
| 178 | elif ch in STAR: |
| 179 | cx, cy, r = STAR[ch] |
| 180 | x, y0 = left + cx * cell_w, top + cy * cell_h |
| 181 | draw.polygon( |
| 182 | [(x, y0 - r * cell_h), (x + r * cell_w, y0), |
| 183 | (x, y0 + r * cell_h), (x - r * cell_w, y0)], |
| 184 | fill=colour, |
| 185 | ) |
| 186 | else: |
| 187 | alpha = 90 if ch in SHADED else 255 |
| 188 | for shape in CELL_SHAPES.get(ch, ()): |
| 189 | draw.rectangle(cell(*shape), fill=(*colour, alpha)) |
| 190 | y += cell_h * 4 + pad |
| 191 | img.save(path) |
| 192 | print(f"wrote {path}") |
| 193 | |
| 194 | |
| 195 | def main() -> None: |
| 196 | parser = argparse.ArgumentParser(description=__doc__) |
| 197 | parser.add_argument("--plain", action="store_true", help="no ANSI colour") |
| 198 | parser.add_argument("--png", metavar="PATH", help="also write a PNG (needs Pillow)") |
| 199 | args = parser.parse_args() |
| 200 | colour = not args.plain and sys.stdout.isatty() |
| 201 | |
| 202 | after = AFTER |
| 203 | print("ARCHIVED: former Codewhale empty-state portrait — main vs Signal Cut\n") |
| 204 | for variant in ("classic", "uwu"): |
| 205 | label = "classic" if variant == "classic" else "uwu" |
| 206 | print(f"== {label} ==\n") |
| 207 | before_spout, before_body = BEFORE[variant] |
| 208 | after_spout, after_body = after[variant] |
| 209 | block("BEFORE", paint(variant, before_spout, before_body, colour, old=True)) |
| 210 | block("AFTER", paint(variant, after_spout, after_body, colour)) |
| 211 | block("AFTER, CODEWHALE_ASCII_SAFE=1", ascii_rows(after_spout, after_body)) |
| 212 | widths = ( |
| 213 | max(len(r) for r in [before_spout] + before_body), |
| 214 | max(len(r) for r in [after_spout] + after_body), |
| 215 | ) |
| 216 | print(f" block width: {widths[0]} -> {widths[1]} columns\n") |
| 217 | |
| 218 | if args.png: |
| 219 | render_png(after, args.png) |
| 220 | |
| 221 | |
| 222 | if __name__ == "__main__": |
| 223 | main() |
| 224 |