#!/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). Drawn on a small pixel grid (32x32 master, hand-tuned 16/24) and scaled with NEAREST, in the in-game sight red (core RGB 200,40,48; lighter edge texels 225,120,130) on near-black (#050607). No glow/gradients/blur. 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 RED = (200, 40, 48) # in-game sight red (core texels) RED_EDGE = (225, 120, 130) # lighter AA-edge texel tint # pixel-grid parameters per master grid size N: # (ring radius, ring half-band, bar width (even), gap radius, reach radius) # radii are in pixels from the exact centre (N/2 - 0.5 is the centre pixel # coordinate, so every shape is mirror-symmetric by construction). _GRIDS = { 16: (4.6, 0.6, 2, 2.0, 7.5), 24: (6.6, 0.55, 2, 3.0, 11.5), 32: (9.2, 0.55, 2, 4.0, 14.5), } def _master(n: int) -> Image.Image: """Hand-tuned low-res mark on an n x n pixel grid (no anti-aliasing: hard pixels in the sight's red, the outermost bar texel and ring corners get the lighter edge tint like the sprite's AA texels).""" ring_r, band, bar_w, gap, reach = _GRIDS[n] img = Image.new("RGBA", (n, n), BG + (255,)) px = img.load() c = (n - 1) / 2.0 hw = bar_w / 2.0 for y in range(n): for x in range(n): dx, dy = abs(x - c), abs(y - c) d = (dx * dx + dy * dy) ** 0.5 col = None if abs(d - ring_r) <= band: col = RED # bars: |perp| < hw, along-axis distance in [gap, reach] for along, perp in ((dx, dy), (dy, dx)): if perp < hw and gap <= along <= reach: col = RED_EDGE if along > reach - 1 else RED if col is not None: px[x, y] = col + (255,) return img def _crosshair(size: int, small: bool = False) -> Image.Image: """Retro red crosshair: the 16/24/32 hand-tuned grids are used 1:1 for the small slots; everything else is the 32 master (24 for 48px) scaled with NEAREST (hard pixel edges).""" if small and size in (16, 24, 32): return _master(size) # integer pixel scale only (48 = 2x the 24 grid) keeps pixels square # and the mark exactly centred. grid = 24 if size == 48 else 32 return _master(grid).resize((size, size), Image.NEAREST) 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) + "}") with open(OUT_HEADER, "w", newline="\n") as fh: fh.write( "/* 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())