#!/usr/bin/env python3 """生成关卡左下角等距 X/Y 罗盘(灰箭头 + 青色标注,抗锯齿)。""" from __future__ import annotations import math from pathlib import Path from PIL import Image, ImageDraw, ImageFilter, ImageFont # HUD 显示约 1.55× Unity,2x 出图保证清晰 OUT_W, OUT_H = 802, 430 SS = 4 ARROW = (92, 98, 108, 255) ARROW_EDGE = (58, 62, 70, 255) TEAL = (64, 220, 188, 255) LABEL_STROKE = (32, 36, 40, 230) SHADOW = (0, 0, 0, 48) def _font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: for fp in ( "/System/Library/Fonts/Supplemental/Arial Bold.ttf", "/System/Library/Fonts/Helvetica.ttc", "/Library/Fonts/Arial Bold.ttf", ): try: return ImageFont.truetype(fp, size) except OSError: continue return ImageFont.load_default() def _arrow(draw: ImageDraw.ImageDraw, ox: float, oy: float, angle: float, length: float) -> tuple[float, float]: """沿 angle(屏幕 Y 向上)画一支圆润箭头,返回箭头顶点。""" dx, dy = math.cos(angle), -math.sin(angle) px, py = -dy, dx shaft = 12.5 * SS head_len = 26 * SS head_w = 20 * SS tip_x = ox + dx * length tip_y = oy + dy * length base_x = ox + dx * (length - head_len) base_y = oy + dy * (length - head_len) # 箭身:圆头线段 draw.line([(ox, oy), (base_x, base_y)], fill=ARROW_EDGE, width=int(shaft + 2.4 * SS), joint="curve") draw.line([(ox, oy), (base_x, base_y)], fill=ARROW, width=int(shaft), joint="curve") hx, hy = px * head_w, py * head_w head = [ (tip_x, tip_y), (base_x + hx, base_y + hy), (base_x - hx, base_y - hy), ] draw.polygon(head, fill=ARROW_EDGE) inset = 1.15 * SS inner = [ (tip_x - dx * inset * 0.4, tip_y - dy * inset * 0.4), (base_x + hx * 0.78 + dx * inset, base_y + hy * 0.78 + dy * inset), (base_x - hx * 0.78 + dx * inset, base_y - hy * 0.78 + dy * inset), ] draw.polygon(inner, fill=ARROW) return tip_x, tip_y def render() -> Image.Image: w, h = OUT_W * SS, OUT_H * SS layer = Image.new("RGBA", (w, h), (0, 0, 0, 0)) draw = ImageDraw.Draw(layer) ox = w * 0.50 oy = h * 0.84 length = min(w, h) * 0.46 ang_x = math.atan2(0.5, 1.0) # 2:1 等距 +X → 右上 ang_y = math.atan2(0.5, -1.0) # +Y → 左上 tx, ty = _arrow(draw, ox, oy, ang_x, length) uy, vy = _arrow(draw, ox, oy, ang_y, length) r = 11 * SS draw.ellipse((ox - r - SS, oy - r - SS, ox + r + SS, oy + r + SS), fill=ARROW_EDGE) draw.ellipse((ox - r, oy - r, ox + r, oy + r), fill=ARROW) ir = 5.2 * SS draw.ellipse((ox - ir, oy - ir, ox + ir, oy + ir), fill=TEAL) font = _font(int(60 * SS)) labels = (("Y", uy, vy, ang_y), ("X", tx, ty, ang_x)) stroke = max(2, int(3.6 * SS)) for text, px, py, ang in labels: dx, dy = math.cos(ang), -math.sin(ang) lx = px + dx * 30 * SS ly = py + dy * 30 * SS bbox = draw.textbbox((0, 0), text, font=font) tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1] pos = (lx - tw / 2, ly - th / 2 - 6 * SS) for oxs in range(-stroke, stroke + 1): for oys in range(-stroke, stroke + 1): if oxs * oxs + oys * oys > stroke * stroke: continue draw.text((pos[0] + oxs, pos[1] + oys), text, font=font, fill=LABEL_STROKE) draw.text(pos, text, font=font, fill=TEAL) alpha = layer.split()[3] sh = Image.new("RGBA", (w, h), (0, 0, 0, 0)) sh_draw = ImageDraw.Draw(sh) # 用同一 alpha 做淡阴影 sh.putalpha(alpha.point(lambda a: min(255, int(a * 0.28)) if a else 0)) rgb = Image.new("RGB", (w, h), (0, 0, 0)) sh = Image.merge("RGBA", (*rgb.split(), sh.split()[-1])) sh = sh.filter(ImageFilter.GaussianBlur(radius=1.6 * SS)) composed = Image.new("RGBA", (w, h), (0, 0, 0, 0)) composed.alpha_composite(sh, (int(1.2 * SS), int(1.4 * SS))) composed.alpha_composite(layer) out = composed.resize((OUT_W, OUT_H), Image.Resampling.LANCZOS) alpha = out.split()[3].point(lambda p: 255 if p >= 16 else 0) box = alpha.getbbox() if box: pad = 16 x0 = max(0, box[0] - pad) y0 = max(0, box[1] - pad) x1 = min(out.width, box[2] + pad) y1 = min(out.height, box[3] + pad) out = out.crop((x0, y0, x1, y1)) return out def write(path: Path) -> Path: path.parent.mkdir(parents=True, exist_ok=True) render().save(path, format="PNG", optimize=True) return path if __name__ == "__main__": out = Path(__file__).resolve().parents[1] / "assets/resources/textures/ui/ui-direction.png" write(out) print(f"wrote {out}")