"""The feeder's status screen, drawn after the reference picture (reference/ref_user.png) on its own pixel positions. Every number below is a position in the reference picture (1024 x 1536). The glass in the model spans the picture's x 140.5 .. 886.5, y 178.5 .. 613.5 (the screen hole offset 28.5 px inward, see nf_build.py), so that rectangle is drawn at K pixels per reference pixel and then fitted to 2048 x 1024 (power of two for Unreal); the glass's UVs stretch it back. Font: Bahnschrift (Windows), the nearest to the picture's DIN-like lettering. python nf_screen_ui.py -> textures/T_NF_Screen_UI.png (emissive colour, sRGB) """ import math, os from PIL import Image, ImageDraw, ImageFilter, ImageFont, ImageChops HERE = os.path.dirname(os.path.abspath(__file__)) OUT = os.path.join(HERE, "..", "textures", "T_NF_Screen_UI.png") GX0, GY0, GX1, GY1 = 140.5, 178.5, 886.5, 613.5 # the glass in reference pixels SS = 2 # supersampling K = 2048.0 / (GX1 - GX0) * SS # canvas pixels per reference pixel W, H = int(round((GX1 - GX0) * K)), int(round((GY1 - GY0) * K)) FONT = r"C:\Windows\Fonts\bahnschrift.ttf" BG0 = (9, 22, 33) # screen ground at the edges BG1 = (13, 33, 47) # and in the middle CYAN = (62, 200, 240) # header text, icons BAR0, BAR1 = (48, 182, 228), (98, 228, 255) TRACK = (24, 64, 86) DIM = (34, 86, 112) # dividers, outer rings RING = (58, 138, 176) # the rows' icon rings LABEL = (116, 178, 210) VALUE = (210, 236, 248) TIME = (170, 214, 234) WHITE = (205, 242, 255) SMALL = (78, 194, 234) def P(x, y): return ((x - GX0) * K, (y - GY0) * K) def L(v): return v * K def font(cap_px, weight="Regular"): """a Bahnschrift face whose capital height is cap_px reference pixels""" f = ImageFont.truetype(FONT, 100) try: f.set_variation_by_name(weight) except Exception: pass box = f.getbbox("H") size = 100.0 * L(cap_px) / (box[3] - box[1]) f = ImageFont.truetype(FONT, max(4, int(round(size)))) try: f.set_variation_by_name(weight) except Exception: pass return f def text(d, s, x, cap_top, cap, col, weight="Regular", anchor="l"): f = font(cap, weight) top = f.getbbox("H")[1] w = f.getlength(s) px, py = P(x, cap_top) if anchor == "r": px -= w elif anchor == "c": px -= w / 2 d.text((px, py - top), s, font=f, fill=col) def rrect(d, x0, y0, x1, y1, r, fill=None, outline=None, width=1.0): d.rounded_rectangle([P(x0, y0), P(x1, y1)], radius=L(r), fill=fill, outline=outline, width=max(1, int(round(L(width))))) def circle(d, cx, cy, r, fill=None, outline=None, width=1.0): d.ellipse([P(cx - r, cy - r), P(cx + r, cy + r)], fill=fill, outline=outline, width=max(1, int(round(L(width))))) def line(d, pts, col, width): d.line([P(*p) for p in pts], fill=col, width=max(1, int(round(L(width)))), joint="curve") def arc(d, cx, cy, r, a0, a1, col, width): """a0, a1 in degrees clockwise from 12 o'clock""" d.arc([P(cx - r, cy - r), P(cx + r, cy + r)], a0 - 90, a1 - 90, fill=col, width=max(1, int(round(L(width))))) def drop_outline(cx, top, bottom, half_w): """a drop: a circle at the bottom and two tangents to the point at the top, as a polygon""" r = half_w cy = bottom - r b = math.acos(min(1.0, r / (cy - top))) # tangent points sit b either side of straight up t0, t1 = -math.pi / 2 + b, 1.5 * math.pi - b # right tangent point, round the bottom, to the left one (y down) n = 40 return [(cx, top)] + [(cx + r * math.cos(t0 + (t1 - t0) * i / n), cy + r * math.sin(t0 + (t1 - t0) * i / n)) for i in range(n + 1)] def lightning(cx, cy): s = [(3.5, -14), (-8.5, 2.5), (-0.5, 2.5), (-4.0, 14.5), (8.5, -3.0), (0.5, -3.0), (5.5, -14)] return [(cx + x, cy + y) for x, y in s] def shield(cx, cy): s = [(0, -14), (11, -9.5), (11, -1), (8.5, 6.5), (0, 13.5), (-8.5, 6.5), (-11, -1), (-11, -9.5)] return [(cx + x, cy + y) for x, y in s] def draw(): base = Image.new("RGB", (W, H), BG0) # ground: darker toward the edges, a faint lift in the middle g = Image.radial_gradient("L").resize((W, H)) mid = Image.new("RGB", (W, H), BG1) base = Image.composite(base, mid, g.point(lambda v: min(255, int(v * 1.15)))) glow = Image.new("RGB", (W, H), (0, 0, 0)) # what glows (blurred and added at the end) d, gd = ImageDraw.Draw(base), ImageDraw.Draw(glow) def both(fn, *a, **k): fn(d, *a, **k) fn(gd, *a, **k) # header both(rrect, 176, 203, 206, 233, 4, outline=CYAN, width=2.0) dr = drop_outline(191, 208.5, 227.5, 6.5) for dd in (d, gd): dd.polygon([P(*p) for p in dr], outline=CYAN, width=max(1, int(round(L(1.8))))) both(circle, 191, 222.5, 2.4, outline=CYAN, width=1.6) for dd in (d, gd): text(dd, "NUTRIENT STATUS", 222, 209, 19, CYAN, "SemiBold") text(d, "14:32", 856, 199.5, 13.5, TIME, "Regular", "r") text(d, "2026-05-16", 856, 222, 11, TIME, "Regular", "r") line(d, [(175, 245.5), (857, 245.5)], DIM, 1.6) line(d, [(601, 250), (601, 592)], DIM, 1.6) rows = [("ENERGY", "82%", 482.5), ("HYDRATION", "76%", 471.5), ("PROTEIN", "64%", 426), ("VITAMINS", "91%", 518)] for k, (name, val, end) in enumerate(rows): vy = 307 + 80 * k both(circle, 203, vy, 26.5, outline=RING, width=2.0) if k == 0: for dd in (d, gd): dd.polygon([P(*p) for p in lightning(203, vy)], fill=CYAN) elif k == 1: dr = drop_outline(203, vy - 16, vy + 14, 11.5) for dd in (d, gd): dd.polygon([P(*p) for p in dr], fill=CYAN) d.arc([P(197 - 6.5, vy + 2 - 6.5), P(197 + 6.5, vy + 2 + 6.5)], 95, 175, fill=(235, 250, 255), width=max(1, int(round(L(2.2))))) elif k == 2: hexp = [(203 + 8.5 * math.cos(math.radians(a)), vy + 8.5 * math.sin(math.radians(a))) for a in range(-90, 270, 60)] for dd in (d, gd): dd.polygon([P(*p) for p in hexp], outline=CYAN, width=max(1, int(round(L(2.0))))) for a in range(-90, 270, 60): c, s = math.cos(math.radians(a)), math.sin(math.radians(a)) line(dd, [(203 + 8.5 * c, vy + 8.5 * s), (203 + 13.5 * c, vy + 13.5 * s)], CYAN, 2.0) circle(dd, 203 + 15.5 * c, vy + 15.5 * s, 2.6, fill=CYAN) else: for dd in (d, gd): dd.polygon([P(*p) for p in shield(203, vy)], outline=CYAN, width=max(1, int(round(L(2.0))))) line(dd, [(197.5, vy + 0.5), (201.5, vy + 4.5), (208.5, vy - 4.5)], CYAN, 2.4) text(d, name, 259, vy - 19, 14, LABEL, "Regular") text(d, val, 561, vy - 19, 14, VALUE, "Regular", "r") rrect(d, 259, vy + 6, 561, vy + 20, 5, fill=TRACK) # fill with a horizontal gradient x0, y0 = P(259, vy + 6) x1, y1 = P(end, vy + 20) bar = Image.new("RGB", (int(x1 - x0), int(y1 - y0))) bd = ImageDraw.Draw(bar) for i in range(bar.width): t = i / max(1, bar.width - 1) bd.line([(i, 0), (i, bar.height)], fill=tuple(int(BAR0[c] + (BAR1[c] - BAR0[c]) * t) for c in range(3))) m = Image.new("L", bar.size, 0) ImageDraw.Draw(m).rounded_rectangle([0, 0, bar.width - 1, bar.height - 1], radius=L(5), fill=255) base.paste(bar, (int(x0), int(y0)), m) glow.paste(bar, (int(x0), int(y0)), m) # right: the ring, the drop with the pulse, the state circle(d, 741, 358, 88, outline=(22, 58, 78), width=1.2) for a in range(0, 360, 30): c, s = math.sin(math.radians(a)), -math.cos(math.radians(a)) line(d, [(741 + 84 * c, 358 + 84 * s), (741 + 92 * c, 358 + 92 * s)], (26, 66, 88), 1.2) both(arc, 741, 358, 69, 0, 298, (74, 212, 252), 8.0) arc(d, 741, 358, 69, 298, 360, (40, 112, 142), 8.0) dr = drop_outline(741, 313, 397, 28) for dd in (d, gd): dd.polygon([P(*p) for p in dr], outline=(74, 212, 252), width=max(1, int(round(L(3.0))))) line(dd, [(714, 358), (731, 358), (736.5, 340), (743, 376), (749, 352), (753, 358), (768, 358)], (74, 212, 252), 3.0) for dd in (d, gd): text(dd, "INFUSION READY", 740, 457, 18, WHITE, "SemiBold", "c") line(d, [(640, 501), (842, 501)], DIM, 1.6) for i, s in enumerate(("DIRECT NUTRIENT DELIVERY", "SYSTEM", "ALL SYSTEMS ONLINE")): text(d, s, 641, 521.5 + 20 * i, 10.5, SMALL, "Regular") # glow: two blurs added on top g1 = glow.filter(ImageFilter.GaussianBlur(L(2.5))) g2 = glow.filter(ImageFilter.GaussianBlur(L(8.0))) out = ImageChops.add(base, Image.eval(g1, lambda v: int(v * 0.35))) out = ImageChops.add(out, Image.eval(g2, lambda v: int(v * 0.30))) out = out.resize((2048, 1024), Image.LANCZOS) os.makedirs(os.path.dirname(OUT), exist_ok=True) out.save(OUT) # a check picture at the glass's true proportion out.resize((2048, int(2048 * (GY1 - GY0) / (GX1 - GX0))), Image.LANCZOS).save(os.path.join(HERE, "..", "reference", "check_screen_ui.png")) return OUT if __name__ == "__main__": print(draw())