#!/usr/bin/env python3 """Generate the ThreadCount Staff launcher icon. python3 scripts/make-staff-icon.py The two apps have to be the same family and still be telling apart on a home screen, where the only other clue is a truncated label. So this is the counter app's mark exactly — four stepped bars in the 60/48/34/22 proportions from tc-icon.svg — inverted onto ink: the leading bar keeps the accent, the rest go from ink to paper, and the ground goes from paper to ink. Light tile versus dark tile is the strongest difference available at 48px, it costs no new artwork, and it stays on-brand: the app's own splash is already ink. The geometry is measured from the counter app's own PNGs rather than guessed, and expressed as fractions of the canvas so every density comes out identical rather than merely similar. Bars are drawn at 4x and downsampled, which is what keeps a 43-pixel-wide bar from landing half a pixel off at mdpi. """ import os from PIL import Image ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) RES = os.path.join(ROOT, "android-staff/app/src/main/res") INK = (32, 30, 29, 255) PAPER = (243, 242, 242, 255) ACCENT = (236, 48, 19, 255) # Measured from android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png (432px): # bars start at x=129, widths 174/139/98/64, first at y=147, pitch 40, height 23. FG = { "x0": 129 / 432, "widths": [174 / 432, 139 / 432, 98 / 432, 64 / 432], "y0": 147 / 432, "pitch": 40 / 432, "height": 23 / 432, } # Measured from .../mipmap-xxxhdpi/ic_launcher.png (192px): the legacy tile crops tighter, so the # mark sits larger in frame than it does inside the adaptive icon's safe zone. LEGACY = { "x0": 38 / 192, "widths": [116 / 192, 93 / 192, 66 / 192, 43 / 192], "y0": 47 / 192, "pitch": 27 / 192, "height": 21 / 192, } SS = 4 # supersample factor def draw(size, geom, background): """The mark at `size` px. `background` None gives a transparent adaptive foreground.""" n = size * SS im = Image.new("RGBA", (n, n), background or (0, 0, 0, 0)) px = Image.new("RGBA", (1, 1)) # placeholder so the linter sees px used below del px for i, wfrac in enumerate(geom["widths"]): x0 = round(geom["x0"] * n) y0 = round((geom["y0"] + i * geom["pitch"]) * n) w = round(wfrac * n) h = round(geom["height"] * n) # The leading bar carries the accent; the rest are paper, because the ground is now ink. colour = ACCENT if i == 0 else PAPER im.paste(colour, (x0, y0, x0 + w, y0 + h)) return im.resize((size, size), Image.LANCZOS) DENSITIES = {"mdpi": (48, 108), "hdpi": (72, 162), "xhdpi": (96, 216), "xxhdpi": (144, 324), "xxxhdpi": (192, 432)} def main(): if not os.path.isdir(RES): raise SystemExit(f"no android-staff res dir at {RES}") for density, (legacy_px, fg_px) in DENSITIES.items(): d = os.path.join(RES, f"mipmap-{density}") os.makedirs(d, exist_ok=True) draw(fg_px, FG, None).save(os.path.join(d, "ic_launcher_foreground.png")) tile = draw(legacy_px, LEGACY, INK) tile.save(os.path.join(d, "ic_launcher.png")) # The launcher applies its own mask, so the round variant is the same tile — which is what # the counter app ships too. tile.save(os.path.join(d, "ic_launcher_round.png")) print(f" {density}: foreground {fg_px}px, launcher {legacy_px}px") bg = os.path.join(RES, "values/ic_launcher_background.xml") with open(bg, "w", encoding="utf-8") as f: f.write('\n\n' ' \n' ' #201E1D\n\n') print(f" background -> #201E1D") if __name__ == "__main__": main()