返回 CodeWhale
preview-whale-art.py
根目录 / scripts / preview-whale-art.py
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
224 lines PYTHON