#!/usr/bin/env python3 """Generate the project's own icon (original art). The mark is an ORIGINAL generic version of the GE default aim crosshair: a circle with four lines (N/S/E/W) crossing the circle's edge and extending well past it, open at the centre. Proportions are measured from the game's own default crosshair (thin ring, long bars, open centre). Site gold (#e8a13a) on near-black (#050607). A circle-plus-ticks reticle is a generic functional shape (no logo art, wordmark or screenshot copied, cf. tree-hygiene rule 4: repo imagery must not be derivative game content). Outputs (all regenerated byte-identically; deterministic): docs/img/icon/ge007-icon.png 512x512 favicon / README icon cmake/icon/ge007.ico multi-size Windows exe icon (16..256) docs/favicon.ico byte copy of ge007.ico (site favicon) port/fast3d/ge007_icon.h 32x32 RGBA array for the SDL window icon (SDL_SetWindowIcon, see gfx_sdl2.cpp) Usage: python tools_pc/make_icon.py # write all three python tools_pc/make_icon.py --check # exit 1 if any output is missing, # or the mark is not exactly centred Requires Pillow (>=10.1, for load_default(size)). """ from __future__ import annotations import io import struct import sys from pathlib import Path try: from PIL import Image, ImageDraw except ImportError: sys.exit("Pillow is required: pip install Pillow") ROOT = Path(__file__).resolve().parent.parent OUT_ICON = ROOT / "docs" / "img" / "icon" / "ge007-icon.png" OUT_ICO = ROOT / "cmake" / "icon" / "ge007.ico" OUT_HEADER = ROOT / "port" / "fast3d" / "ge007_icon.h" OUT_FAVICON = ROOT / "docs" / "favicon.ico" # --- palette ----------------------------------------------------------------- BG = (5, 6, 7) # site near-black GOLD = (232, 161, 58) # site gold (#e8a13a) def _crosshair(size: int, small: bool = False) -> Image.Image: """The generic GE default crosshair: a circle with N/S/E/W lines crossing its edge and extending well past it, open centre, in site gold (#e8a13a); proportions measured from the game's own default crosshair (thin ring, long bars, open centre). Rendered at 2x for AA, then downsized. small: a chunkier variant (thicker strokes, wider gap) so the mark survives 16px in the .ico small slots (and the 32px window icon).""" S = size * 2 img = Image.new("RGBA", (S, S), BG + (255,)) dr = ImageDraw.Draw(img, "RGBA") # Exact centre = the line between the two middle supersample pixels # (continuous coordinate S/2; pixel i covers [i, i+1)). Every shape is # drawn symmetric about it with inclusive PIL coordinates, so each size # is pixel-exactly mirror-symmetric (checked by _symmetric()). c = S // 2 def rr(v: float) -> int: return int(round(S * v / 512)) def even(v: float) -> int: return max(2, 2 * int(round(S * v / 512 / 2))) def disc(r: int) -> list: return [c - r, c - r, c + r - 1, c + r - 1] # faint gold glow behind the reticle (kept very subtle: flat + calm) glow = Image.new("RGBA", (S, S), (0, 0, 0, 0)) gd = ImageDraw.Draw(glow) gr = rr(190 if small else 200) for i in range(24, 0, -1): g = gr * i // 24 if g > 0: gd.ellipse(disc(g), fill=GOLD + (4,)) img.alpha_composite(glow) # the reticle: a circle + four bars (N/S/E/W) crossing the circle's # edge and extending well past it, with an open centre -- proportions # measured off the game's own default crosshair (thin ring, long bars; # bar reach ~1.45x the ring's outer radius, centre gap ~0.30x). # Small sizes get the chunky variant (thicker strokes, wider gap so # it stays open at 16px). circle_r = rr(140 if small else 150) # outer radius of the ring ring_w = rr(34 if small else 16) # ring stroke width bar_w = even(30 if small else 14) # bar stroke width (even: symmetric) reach = rr(205 if small else 218) gap = rr(52 if small else 45) hw = bar_w // 2 dr.ellipse(disc(circle_r), outline=GOLD + (255,), width=ring_w) for a, b in ((c - reach, c - gap - 1), (c + gap, c + reach - 1)): dr.rectangle([c - hw, a, c + hw - 1, b], fill=GOLD + (255,)) # vertical dr.rectangle([a, c - hw, b, c + hw - 1], fill=GOLD + (255,)) # horizontal return img.resize((size, size), Image.LANCZOS) def _symmetric(img: Image.Image) -> bool: """Pixel-exact left-right and top-bottom mirror symmetry: the mark is exactly centred at this size. (Not the diagonal: LANCZOS resamples the two axes in a fixed order, so a transpose differs by rounding only.)""" return (img.tobytes() == img.transpose(Image.FLIP_LEFT_RIGHT).tobytes() and img.tobytes() == img.transpose(Image.FLIP_TOP_BOTTOM).tobytes()) def _asymmetric_sizes() -> list: sizes = [(512, False), (16, True), (24, True), (32, True), (48, False), (64, False), (128, False), (256, False)] return [f"{s}{' (small)' if sm else ''}" for s, sm in sizes if not _symmetric(_crosshair(s, small=sm))] def _ico_png(size: int, small: bool) -> bytes: buf = io.BytesIO() _crosshair(size, small=small).save(buf, format="PNG") return buf.getvalue() def make_ico() -> None: _crosshair(512).save(OUT_ICON) # Multi-size .ico with PNG-compressed entries (Vista+ style; what the # 256 slot always is). Small sizes get the chunky variant so the mark # survives 16px; a single 512-master downsample washes out there. entries = [(_ico_png(s, small=True), s) for s in (16, 24, 32)] entries += [(_ico_png(s, small=False), s) for s in (48, 64, 128, 256)] n = len(entries) offset = 6 + 16 * n dirs, data = b"", b"" for png, s in entries: wh = 0 if s == 256 else s # 0 means 256 in the ICO format dirs += struct.pack(" None: """Emit the 32x32 RGBA array for the SDL window icon (gfx_sdl2.cpp). Byte order is plain R,G,B,A per pixel (portable; the C++ side packs it against explicit masks). NOTE: this output is C, and CMake compiles it inside gfx_sdl2.cpp's include — it is NOT independently syntax-checked by this script. Keep it trivially valid (see the row-brace comment below; a flat cell list without row braces is ill-formed for a 3-D array).""" img = _crosshair(32, small=True).convert("RGBA") px = img.load() rows = [] for y in range(32): row = [] for x in range(32): r, g, b, a = px[x, y] row.append(f"{{{r:3d},{g:3d},{b:3d},{a:3d}}}") # Row braces are REQUIRED: without them all 1024 cells sit at the # top level of the [32][32][4] initializer (1024 > first dim 32 -> # "too many initializers"). rows.append(" {" + ",".join(row) + "}") OUT_HEADER.write_text( "/* Generated by tools_pc/make_icon.py -- do not edit by hand.\n" " * The project mark (a generic version of the GE default aim\n" " * crosshair) as a 32x32 RGBA array, row-major, plain R,G,B,A\n" " * byte order per pixel; used by gfx_sdl2.cpp for\n" " * SDL_SetWindowIcon. Regenerate: python tools_pc/make_icon.py */\n" "#ifndef GE007_ICON_H\n" "#define GE007_ICON_H\n" "\n" "#include \n" "\n" "#define GE007_ICON_SIZE 32\n" "static const uint8_t ge007_icon_rgba[GE007_ICON_SIZE]" "[GE007_ICON_SIZE][4] = {\n" + ",\n".join(rows) + "\n};\n" "#endif /* GE007_ICON_H */\n" ) def main() -> int: args = sys.argv[1:] check = "--check" in args for p in (OUT_ICON.parent, OUT_ICO.parent, OUT_HEADER.parent): p.mkdir(parents=True, exist_ok=True) bad = _asymmetric_sizes() if bad: print("icon is not exactly centred (mirror-asymmetric) at: " + ", ".join(bad)) return 1 if not check: make_ico() make_header() print(f"wrote {OUT_ICON.relative_to(ROOT)}") print(f"wrote {OUT_ICO.relative_to(ROOT)}") print(f"wrote {OUT_FAVICON.relative_to(ROOT)}") print(f"wrote {OUT_HEADER.relative_to(ROOT)}") return 0 stale = [p for p in (OUT_ICON, OUT_ICO, OUT_HEADER, OUT_FAVICON) if not p.exists()] if stale: for p in stale: print(f"stale/missing: {p.relative_to(ROOT)}") return 1 print("ok (all icon outputs present and mirror-symmetric at every size; " "regenerate with: python tools_pc/make_icon.py)") return 0 if __name__ == "__main__": sys.exit(main())