324 lines
12 KiB
Python
324 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Iterable
|
|
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
OUTPUT_DIR = REPO_ROOT / "public" / "branding" / "taonier-logo-anchor-concepts"
|
|
CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-anchor-contact-sheet.png"
|
|
|
|
SIZE = 1024
|
|
SCALE = 4
|
|
|
|
VARIANTS = [
|
|
{
|
|
"id": "taonier-anchor-core",
|
|
"title": "泥点锚标",
|
|
"bg": "#151515",
|
|
"mark": "#ffffff",
|
|
"accent": "#ffffff",
|
|
"style": "core",
|
|
},
|
|
{
|
|
"id": "taonier-anchor-soft-slab",
|
|
"title": "软泥层台",
|
|
"bg": "#111111",
|
|
"mark": "#fffdf4",
|
|
"accent": "#fffdf4",
|
|
"style": "soft_slab",
|
|
},
|
|
{
|
|
"id": "taonier-anchor-work-stack",
|
|
"title": "作品叠层",
|
|
"bg": "#171717",
|
|
"mark": "#ffffff",
|
|
"accent": "#ffffff",
|
|
"style": "work_stack",
|
|
},
|
|
{
|
|
"id": "taonier-anchor-clay-drop",
|
|
"title": "泥点落印",
|
|
"bg": "#151515",
|
|
"mark": "#ffffff",
|
|
"accent": "#f5c95d",
|
|
"style": "clay_drop",
|
|
},
|
|
{
|
|
"id": "taonier-anchor-creation-base",
|
|
"title": "创作底座",
|
|
"bg": "#121212",
|
|
"mark": "#ffffff",
|
|
"accent": "#ffffff",
|
|
"style": "creation_base",
|
|
},
|
|
{
|
|
"id": "taonier-anchor-app-token",
|
|
"title": "泥点应用标",
|
|
"bg": "#101418",
|
|
"mark": "#fffaf0",
|
|
"accent": "#ffd45d",
|
|
"style": "app_token",
|
|
},
|
|
]
|
|
|
|
|
|
def hex_to_rgb(value: str) -> tuple[int, int, int]:
|
|
value = value.removeprefix("#")
|
|
return tuple(int(value[index : index + 2], 16) for index in (0, 2, 4))
|
|
|
|
|
|
def s(value: float) -> int:
|
|
return round(value * SCALE)
|
|
|
|
|
|
def scaled_points(points: Iterable[tuple[float, float]]) -> list[tuple[int, int]]:
|
|
return [(s(x), s(y)) for x, y in points]
|
|
|
|
|
|
def quad(
|
|
start: tuple[float, float],
|
|
control: tuple[float, float],
|
|
end: tuple[float, float],
|
|
steps: int = 24,
|
|
) -> list[tuple[float, float]]:
|
|
points: list[tuple[float, float]] = []
|
|
for index in range(steps + 1):
|
|
t = index / steps
|
|
x = (1 - t) * (1 - t) * start[0] + 2 * (1 - t) * t * control[0] + t * t * end[0]
|
|
y = (1 - t) * (1 - t) * start[1] + 2 * (1 - t) * t * control[1] + t * t * end[1]
|
|
points.append((x, y))
|
|
return points
|
|
|
|
|
|
def round_line(
|
|
draw: ImageDraw.ImageDraw,
|
|
points: list[tuple[float, float]],
|
|
fill: str,
|
|
width: int,
|
|
closed: bool = False,
|
|
) -> None:
|
|
scaled = scaled_points(points)
|
|
if closed:
|
|
scaled = [*scaled, scaled[0]]
|
|
draw.line(scaled, fill=hex_to_rgb(fill), width=s(width), joint="curve")
|
|
radius = s(width) // 2
|
|
if not closed:
|
|
for x, y in (scaled[0], scaled[-1]):
|
|
draw.ellipse((x - radius, y - radius, x + radius, y + radius), fill=hex_to_rgb(fill))
|
|
|
|
|
|
def round_circle(draw: ImageDraw.ImageDraw, center: tuple[float, float], radius: float, fill: str) -> None:
|
|
x, y = center
|
|
draw.ellipse((s(x - radius), s(y - radius), s(x + radius), s(y + radius)), fill=hex_to_rgb(fill))
|
|
|
|
|
|
def draw_core(draw: ImageDraw.ImageDraw, mark: str, accent: str) -> None:
|
|
round_line(draw, [(512, 326), (512, 514)], mark, 68)
|
|
round_circle(draw, (512, 214), 62, accent)
|
|
round_line(draw, [(244, 548), (512, 430), (780, 548), (512, 674)], mark, 66, closed=True)
|
|
round_line(draw, [(292, 656), (468, 734), (512, 752), (556, 734), (732, 656)], mark, 52)
|
|
round_circle(draw, (337, 548), 17, mark)
|
|
|
|
|
|
def draw_soft_slab(draw: ImageDraw.ImageDraw, mark: str, accent: str) -> None:
|
|
round_line(draw, [(512, 316), (512, 518)], mark, 72)
|
|
round_circle(draw, (512, 205), 58, accent)
|
|
top = (
|
|
quad((232, 560), (512, 410), (792, 560), 32)
|
|
+ quad((792, 560), (816, 576), (792, 592), 8)[1:]
|
|
+ quad((792, 592), (610, 648), (552, 692), 20)[1:]
|
|
+ quad((552, 692), (512, 716), (472, 692), 12)[1:]
|
|
+ quad((472, 692), (414, 648), (232, 592), 20)[1:]
|
|
+ quad((232, 592), (208, 576), (232, 560), 8)[1:]
|
|
)
|
|
round_line(draw, top, mark, 56, closed=True)
|
|
round_line(draw, [(278, 642), (470, 728), (512, 748), (554, 728), (746, 642)], mark, 46)
|
|
round_circle(draw, (342, 554), 15, mark)
|
|
|
|
|
|
def draw_work_stack(draw: ImageDraw.ImageDraw, mark: str, accent: str) -> None:
|
|
round_line(draw, [(512, 310), (512, 504)], mark, 64)
|
|
round_circle(draw, (512, 205), 56, accent)
|
|
round_line(draw, [(246, 532), (512, 420), (778, 532), (512, 650)], mark, 58, closed=True)
|
|
round_line(draw, [(286, 628), (472, 710), (512, 728), (552, 710), (738, 628)], mark, 48)
|
|
round_line(draw, [(330, 708), (478, 774), (512, 790), (546, 774), (694, 708)], mark, 38)
|
|
round_circle(draw, (348, 535), 14, mark)
|
|
|
|
|
|
def draw_clay_drop(draw: ImageDraw.ImageDraw, mark: str, accent: str) -> None:
|
|
round_line(draw, [(512, 334), (512, 504)], mark, 64)
|
|
round_circle(draw, (512, 204), 62, accent)
|
|
round_line(draw, [(252, 548), (512, 436), (772, 548), (512, 666)], mark, 62, closed=True)
|
|
round_line(draw, [(304, 642), (474, 718), (512, 736), (550, 718), (720, 642)], mark, 50)
|
|
round_circle(draw, (346, 548), 16, mark)
|
|
round_circle(draw, (512, 556), 12, accent)
|
|
|
|
|
|
def draw_creation_base(draw: ImageDraw.ImageDraw, mark: str, accent: str) -> None:
|
|
round_line(draw, [(512, 304), (512, 494)], mark, 58)
|
|
round_circle(draw, (512, 202), 55, accent)
|
|
round_line(draw, [(276, 522), (512, 420), (748, 522)], mark, 54)
|
|
round_line(draw, [(236, 586), (512, 708), (788, 586)], mark, 60)
|
|
round_line(draw, [(292, 676), (478, 756), (512, 770), (546, 756), (732, 676)], mark, 42)
|
|
round_circle(draw, (355, 544), 13, mark)
|
|
|
|
|
|
def draw_app_token(draw: ImageDraw.ImageDraw, mark: str, accent: str) -> None:
|
|
round_line(draw, [(512, 326), (512, 508)], mark, 70)
|
|
round_circle(draw, (512, 208), 62, accent)
|
|
round_line(draw, [(252, 548), (512, 432), (772, 548), (512, 674)], mark, 66, closed=True)
|
|
round_line(draw, [(296, 656), (470, 732), (512, 752), (554, 732), (728, 656)], mark, 52)
|
|
round_circle(draw, (338, 548), 15, accent)
|
|
|
|
|
|
DRAWERS = {
|
|
"core": draw_core,
|
|
"soft_slab": draw_soft_slab,
|
|
"work_stack": draw_work_stack,
|
|
"clay_drop": draw_clay_drop,
|
|
"creation_base": draw_creation_base,
|
|
"app_token": draw_app_token,
|
|
}
|
|
|
|
|
|
def build_svg(variant: dict[str, str]) -> str:
|
|
bg = variant["bg"]
|
|
mark = variant["mark"]
|
|
accent = variant["accent"]
|
|
style = variant["style"]
|
|
shared = 'fill="none" stroke-linecap="round" stroke-linejoin="round"'
|
|
dot_fill = accent if style in {"clay_drop", "app_token"} else mark
|
|
left_dot_fill = accent if style == "app_token" else mark
|
|
|
|
if style == "soft_slab":
|
|
base = f'''
|
|
<path d="M232 560 Q512 410 792 560 Q816 576 792 592 Q610 648 552 692 Q512 716 472 692 Q414 648 232 592 Q208 576 232 560 Z" {shared} stroke="{mark}" stroke-width="56"/>
|
|
<path d="M278 642 L470 728 Q512 748 554 728 L746 642" {shared} stroke="{mark}" stroke-width="46"/>
|
|
<circle cx="342" cy="554" r="15" fill="{mark}"/>'''
|
|
stem = f'<path d="M512 316 L512 518" {shared} stroke="{mark}" stroke-width="72"/>'
|
|
dot = f'<circle cx="512" cy="205" r="58" fill="{dot_fill}"/>'
|
|
elif style == "work_stack":
|
|
base = f'''
|
|
<path d="M246 532 L512 420 L778 532 L512 650 Z" {shared} stroke="{mark}" stroke-width="58"/>
|
|
<path d="M286 628 L472 710 Q512 728 552 710 L738 628" {shared} stroke="{mark}" stroke-width="48"/>
|
|
<path d="M330 708 L478 774 Q512 790 546 774 L694 708" {shared} stroke="{mark}" stroke-width="38"/>
|
|
<circle cx="348" cy="535" r="14" fill="{mark}"/>'''
|
|
stem = f'<path d="M512 310 L512 504" {shared} stroke="{mark}" stroke-width="64"/>'
|
|
dot = f'<circle cx="512" cy="205" r="56" fill="{dot_fill}"/>'
|
|
elif style == "clay_drop":
|
|
base = f'''
|
|
<path d="M252 548 L512 436 L772 548 L512 666 Z" {shared} stroke="{mark}" stroke-width="62"/>
|
|
<path d="M304 642 L474 718 Q512 736 550 718 L720 642" {shared} stroke="{mark}" stroke-width="50"/>
|
|
<circle cx="346" cy="548" r="16" fill="{mark}"/>
|
|
<circle cx="512" cy="556" r="12" fill="{accent}"/>'''
|
|
stem = f'<path d="M512 334 L512 504" {shared} stroke="{mark}" stroke-width="64"/>'
|
|
dot = f'<circle cx="512" cy="204" r="62" fill="{dot_fill}"/>'
|
|
elif style == "creation_base":
|
|
base = f'''
|
|
<path d="M276 522 L512 420 L748 522" {shared} stroke="{mark}" stroke-width="54"/>
|
|
<path d="M236 586 L512 708 L788 586" {shared} stroke="{mark}" stroke-width="60"/>
|
|
<path d="M292 676 L478 756 Q512 770 546 756 L732 676" {shared} stroke="{mark}" stroke-width="42"/>
|
|
<circle cx="355" cy="544" r="13" fill="{mark}"/>'''
|
|
stem = f'<path d="M512 304 L512 494" {shared} stroke="{mark}" stroke-width="58"/>'
|
|
dot = f'<circle cx="512" cy="202" r="55" fill="{dot_fill}"/>'
|
|
else:
|
|
stroke = 70 if style == "app_token" else 68
|
|
base_width = 66
|
|
layer_width = 52
|
|
base = f'''
|
|
<path d="M244 548 L512 430 L780 548 L512 674 Z" {shared} stroke="{mark}" stroke-width="{base_width}"/>
|
|
<path d="M292 656 L468 734 Q512 752 556 734 L732 656" {shared} stroke="{mark}" stroke-width="{layer_width}"/>
|
|
<circle cx="337" cy="548" r="{15 if style == 'app_token' else 17}" fill="{left_dot_fill}"/>'''
|
|
stem = f'<path d="M512 326 L512 514" {shared} stroke="{mark}" stroke-width="{stroke}"/>'
|
|
dot = f'<circle cx="512" cy="{208 if style == "app_token" else 214}" r="62" fill="{dot_fill}"/>'
|
|
|
|
return f'''<?xml version="1.0" encoding="UTF-8"?>
|
|
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
|
<rect width="1024" height="1024" rx="160" fill="{bg}"/>
|
|
{base}
|
|
{stem}
|
|
{dot}
|
|
</svg>
|
|
'''
|
|
|
|
|
|
def render_variant(variant: dict[str, str]) -> Image.Image:
|
|
image = Image.new("RGB", (SIZE * SCALE, SIZE * SCALE), hex_to_rgb(variant["bg"]))
|
|
draw = ImageDraw.Draw(image)
|
|
DRAWERS[variant["style"]](draw, variant["mark"], variant["accent"])
|
|
return image.resize((SIZE, SIZE), Image.Resampling.LANCZOS)
|
|
|
|
|
|
def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
|
|
candidates = [
|
|
Path("C:/Windows/Fonts/msyh.ttc"),
|
|
Path("C:/Windows/Fonts/simhei.ttf"),
|
|
Path("C:/Windows/Fonts/simsun.ttc"),
|
|
]
|
|
for candidate in candidates:
|
|
if candidate.exists():
|
|
return ImageFont.truetype(str(candidate), size)
|
|
return ImageFont.load_default()
|
|
|
|
|
|
def build_contact_sheet(previews: list[tuple[dict[str, str], Image.Image]]) -> Image.Image:
|
|
cell_size = 320
|
|
label_height = 60
|
|
gap = 28
|
|
columns = 3
|
|
rows = 2
|
|
width = columns * cell_size + (columns + 1) * gap
|
|
height = rows * (cell_size + label_height) + (rows + 1) * gap
|
|
sheet = Image.new("RGB", (width, height), "#eee9df")
|
|
draw = ImageDraw.Draw(sheet)
|
|
font = load_font(23)
|
|
|
|
for index, (variant, preview) in enumerate(previews):
|
|
row = index // columns
|
|
column = index % columns
|
|
x = gap + column * (cell_size + gap)
|
|
y = gap + row * (cell_size + label_height + gap)
|
|
thumbnail = preview.resize((cell_size, cell_size), Image.Resampling.LANCZOS)
|
|
sheet.paste(thumbnail, (x, y))
|
|
draw.rounded_rectangle(
|
|
(x, y + cell_size, x + cell_size, y + cell_size + label_height),
|
|
radius=10,
|
|
fill="#fffdf8",
|
|
)
|
|
label = f"{index + 1:02d} {variant['title']}"
|
|
text_box = draw.textbbox((0, 0), label, font=font)
|
|
text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2
|
|
text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2
|
|
draw.text((text_x, text_y), label, fill="#302a25", font=font)
|
|
return sheet
|
|
|
|
|
|
def main() -> None:
|
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
previews: list[tuple[dict[str, str], Image.Image]] = []
|
|
|
|
for variant in VARIANTS:
|
|
(OUTPUT_DIR / f"{variant['id']}.svg").write_text(build_svg(variant), encoding="utf-8")
|
|
preview = render_variant(variant)
|
|
preview.save(OUTPUT_DIR / f"{variant['id']}.png")
|
|
previews.append((variant, preview))
|
|
|
|
contact_sheet = build_contact_sheet(previews)
|
|
contact_sheet.save(CONTACT_SHEET_PATH, quality=95)
|
|
print(
|
|
{
|
|
"ok": True,
|
|
"output_dir": str(OUTPUT_DIR),
|
|
"files": [f"{variant['id']}.svg" for variant in VARIANTS]
|
|
+ [f"{variant['id']}.png" for variant in VARIANTS]
|
|
+ [CONTACT_SHEET_PATH.name],
|
|
}
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|