同步主站 Cocos 当前工程:关卡资源、运行脚本与打包工具。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
138
tools/make_direction_gizmo.py
Normal file
138
tools/make_direction_gizmo.py
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/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}")
|
||||
Reference in New Issue
Block a user