#!/usr/bin/env python3 """ Regenerates the extension's images. Nova wants each asset in Images//.png plus an @2x variant, and a metadata.json marking it a template image so macOS tints it for light and dark sidebars. The shapes are drawn as unit-square coverage tests and supersampled, which keeps the whole thing dependency-free. Run from the extension root: python3 Tools/make-icons.py """ import math import os import struct import zlib SS = 4 # supersamples per axis def png(path, w, h, rgba): raw = b"".join(b"\x00" + bytes(rgba[y*w*4:(y+1)*w*4]) for y in range(h)) def chunk(tag, data): c = tag + data return struct.pack(">I", len(data)) + c + struct.pack(">I", zlib.crc32(c) & 0xffffffff) out = (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0)) + chunk(b"IDAT", zlib.compress(raw, 9)) + chunk(b"IEND", b"")) os.makedirs(os.path.dirname(path), exist_ok=True) open(path, "wb").write(out) def render(size, shape): """shape(x, y) -> True when the unit-square point (0..1) is inked.""" buf = bytearray(size * size * 4) for py in range(size): for px in range(size): hits = 0 for sy in range(SS): for sx in range(SS): x = (px + (sx + 0.5) / SS) / size y = (py + (sy + 0.5) / SS) / size if shape(x, y): hits += 1 a = int(round(255 * hits / (SS * SS))) i = (py * size + px) * 4 buf[i:i+4] = bytes((0, 0, 0, a)) return buf # -- shape primitives, all in a 0..1 unit square --------------------------- def disc(cx, cy, r): return lambda x, y: (x-cx)**2 + (y-cy)**2 <= r*r def ring(cx, cy, r, w): return lambda x, y: abs(math.hypot(x-cx, y-cy) - r) <= w/2 def arc(cx, cy, r, w, a0, a1): def f(x, y): if abs(math.hypot(x-cx, y-cy) - r) > w/2: return False a = math.degrees(math.atan2(-(y-cy), x-cx)) % 360 lo, hi = a0 % 360, a1 % 360 return lo <= a <= hi if lo <= hi else (a >= lo or a <= hi) return f def seg(x0, y0, x1, y1, w): dx, dy = x1-x0, y1-y0 L2 = dx*dx + dy*dy def f(x, y): t = 0.0 if L2 == 0 else max(0.0, min(1.0, ((x-x0)*dx + (y-y0)*dy) / L2)) return math.hypot(x - (x0+t*dx), y - (y0+t*dy)) <= w/2 return f def tri(p0, p1, p2): def side(a, b, p): return (b[0]-a[0])*(p[1]-a[1]) - (b[1]-a[1])*(p[0]-a[0]) def f(x, y): p = (x, y) d0, d1, d2 = side(p0, p1, p), side(p1, p2, p), side(p2, p0, p) return (d0 >= 0 and d1 >= 0 and d2 >= 0) or (d0 <= 0 and d1 <= 0 and d2 <= 0) return f def rrect(x0, y0, x1, y1, r): def f(x, y): cx = min(max(x, x0+r), x1-r) cy = min(max(y, y0+r), y1-r) if x0 <= x <= x1 and y0 <= y <= y1: return math.hypot(x-cx, y-cy) <= r or (x0+r <= x <= x1-r) or (y0+r <= y <= y1-r) return False return f def union(*shapes): return lambda x, y: any(s(x, y) for s in shapes) def subtract(base, *holes): return lambda x, y: base(x, y) and not any(h(x, y) for h in holes) # -- the icons ------------------------------------------------------------- # A teacup with steam: the Gitea mark, simplified enough to read at 16px. def cup_body(x, y): top, bottom = 0.44, 0.80 if not (top <= y <= bottom): return False t = (y - top) / (bottom - top) half = 0.26 * (1 - 0.42 * t) # tapers toward the base if abs(x - 0.46) > half: return False if t > 0.82: # round the bottom corners off return abs(x - 0.46) <= half * (1 - (t - 0.82) * 3.4) return True GITEA = union( cup_body, arc(0.735, 0.575, 0.105, 0.055, 285, 75), # handle rrect(0.16, 0.83, 0.78, 0.90, 0.035), # saucer seg(0.38, 0.34, 0.38, 0.24, 0.055), # steam seg(0.54, 0.34, 0.54, 0.20, 0.055), ) # Two commits joined by an elbow: the branch-filter control. BRANCH = union( disc(0.28, 0.26, 0.115), disc(0.28, 0.76, 0.115), disc(0.74, 0.42, 0.115), seg(0.28, 0.26, 0.28, 0.76, 0.075), seg(0.28, 0.44, 0.74, 0.44, 0.075), ) # A circular arrow: refresh. REFRESH = union( arc(0.5, 0.52, 0.30, 0.10, 20, 300), tri((0.60, 0.14), (0.88, 0.24), (0.62, 0.38)), ) ICONS = { "gitea-small": (GITEA, [16, 32]), "gitea-large": (GITEA, [32, 64]), "extension": (GITEA, [32, 64]), "refresh": (REFRESH, [16, 32]), "branch": (BRANCH, [16, 32]), } ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) for name, (shape, sizes) in ICONS.items(): folder = os.path.join(ROOT, "Images", name) for index, size in enumerate(sizes): suffix = "" if index == 0 else "@2x" png(os.path.join(folder, f"{name}{suffix}.png"), size, size, render(size, shape)) open(os.path.join(folder, "metadata.json"), "w").write('{\n "template": true\n}\n') print("wrote", folder)