feat: refresh creation config and visual assets

This commit is contained in:
2026-05-20 14:02:36 +08:00
parent 83e92fc3c4
commit ef09a23c35
509 changed files with 19470 additions and 43 deletions

View File

@@ -0,0 +1,323 @@
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()