301 lines
12 KiB
Python
301 lines
12 KiB
Python
from __future__ import annotations
|
||
|
||
import math
|
||
from pathlib import Path
|
||
|
||
from PIL import Image, ImageDraw, ImageFont
|
||
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||
OUTPUT_DIR = REPO_ROOT / "public" / "branding" / "taonier-logo-geometric-concepts"
|
||
CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-geometric-contact-sheet.png"
|
||
|
||
SIZE = 1024
|
||
SCALE = 4
|
||
|
||
INK = "#151515"
|
||
CREAM = "#fff7e6"
|
||
GOLD = "#ffd25d"
|
||
CORAL = "#ff6a5f"
|
||
MINT = "#29c9ad"
|
||
BLUE = "#2f6bff"
|
||
|
||
VARIANTS = [
|
||
("taonier-geometric-offset-core", "偏心泥孔", "offset_core"),
|
||
("taonier-geometric-mold-chip", "模芯切片", "mold_chip"),
|
||
("taonier-geometric-pinched-tile", "捏痕方标", "pinched_tile"),
|
||
("taonier-geometric-dual-plate", "双片合模", "dual_plate"),
|
||
("taonier-geometric-dot-gate", "泥点入口", "dot_gate"),
|
||
("taonier-geometric-work-knot", "作品结点", "work_knot"),
|
||
]
|
||
|
||
|
||
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 rgba(value: str) -> tuple[int, int, int, int]:
|
||
red, green, blue = hex_to_rgb(value)
|
||
return red, green, blue, 255
|
||
|
||
|
||
def regular_polygon(cx: float, cy: float, radius: float, sides: int, rotation: float) -> list[tuple[int, int]]:
|
||
points = []
|
||
for index in range(sides):
|
||
angle = rotation + math.tau * index / sides
|
||
points.append((s(cx + math.cos(angle) * radius), s(cy + math.sin(angle) * radius)))
|
||
return points
|
||
|
||
|
||
def rounded_rectangle(
|
||
draw: ImageDraw.ImageDraw,
|
||
box: tuple[float, float, float, float],
|
||
radius: float,
|
||
fill: str,
|
||
) -> None:
|
||
draw.rounded_rectangle(tuple(s(value) for value in box), radius=s(radius), fill=hex_to_rgb(fill))
|
||
|
||
|
||
def circle(draw: ImageDraw.ImageDraw, cx: float, cy: float, radius: float, fill: str) -> None:
|
||
draw.ellipse((s(cx - radius), s(cy - radius), s(cx + radius), s(cy + radius)), fill=hex_to_rgb(fill))
|
||
|
||
|
||
def draw_offset_core(draw: ImageDraw.ImageDraw) -> None:
|
||
draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#111111"))
|
||
rounded_rectangle(draw, (236, 236, 788, 788), 148, CREAM)
|
||
circle(draw, 610, 456, 116, "#111111")
|
||
circle(draw, 610, 456, 48, GOLD)
|
||
rounded_rectangle(draw, (268, 612, 536, 718), 53, "#111111")
|
||
rounded_rectangle(draw, (294, 638, 500, 690), 26, CREAM)
|
||
circle(draw, 352, 370, 34, "#111111")
|
||
circle(draw, 352, 370, 17, GOLD)
|
||
|
||
|
||
def draw_mold_chip(draw: ImageDraw.ImageDraw) -> None:
|
||
draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#101418"))
|
||
draw.polygon(
|
||
[
|
||
(s(278), s(230)),
|
||
(s(734), s(230)),
|
||
(s(828), s(330)),
|
||
(s(828), s(694)),
|
||
(s(706), s(794)),
|
||
(s(278), s(794)),
|
||
(s(196), s(708)),
|
||
(s(196), s(322)),
|
||
],
|
||
fill=hex_to_rgb(CREAM),
|
||
)
|
||
circle(draw, 512, 512, 144, "#101418")
|
||
circle(draw, 512, 512, 62, GOLD)
|
||
rounded_rectangle(draw, (224, 280, 518, 370), 45, CORAL)
|
||
rounded_rectangle(draw, (574, 654, 796, 736), 41, MINT)
|
||
|
||
|
||
def draw_pinched_tile(draw: ImageDraw.ImageDraw) -> None:
|
||
draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#14100d"))
|
||
rounded_rectangle(draw, (232, 250, 792, 774), 170, CREAM)
|
||
circle(draw, 232, 512, 94, "#14100d")
|
||
circle(draw, 792, 512, 94, "#14100d")
|
||
draw.polygon(regular_polygon(512, 512, 104, 4, math.pi / 4), fill=hex_to_rgb("#14100d"))
|
||
circle(draw, 512, 512, 38, GOLD)
|
||
rounded_rectangle(draw, (420, 300, 604, 358), 29, CORAL)
|
||
rounded_rectangle(draw, (420, 666, 604, 724), 29, MINT)
|
||
|
||
|
||
def draw_dual_plate(draw: ImageDraw.ImageDraw) -> None:
|
||
draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#111111"))
|
||
rounded_rectangle(draw, (214, 316, 790, 464), 74, CORAL)
|
||
rounded_rectangle(draw, (234, 560, 810, 708), 74, MINT)
|
||
draw.polygon(regular_polygon(512, 512, 138, 4, math.pi / 4), fill=hex_to_rgb(CREAM))
|
||
draw.polygon(regular_polygon(512, 512, 76, 4, math.pi / 4), fill=hex_to_rgb("#111111"))
|
||
circle(draw, 512, 512, 32, GOLD)
|
||
circle(draw, 262, 390, 24, CREAM)
|
||
circle(draw, 762, 634, 24, CREAM)
|
||
|
||
|
||
def draw_dot_gate(draw: ImageDraw.ImageDraw) -> None:
|
||
draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#101010"))
|
||
rounded_rectangle(draw, (276, 330, 748, 752), 132, CREAM)
|
||
rounded_rectangle(draw, (386, 440, 638, 752), 126, "#101010")
|
||
circle(draw, 512, 260, 62, GOLD)
|
||
rounded_rectangle(draw, (450, 308, 574, 504), 62, CREAM)
|
||
circle(draw, 512, 518, 40, GOLD)
|
||
rounded_rectangle(draw, (316, 754, 708, 812), 29, CREAM)
|
||
|
||
|
||
def draw_work_knot(draw: ImageDraw.ImageDraw) -> None:
|
||
draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#121212"))
|
||
circle(draw, 396, 402, 132, CREAM)
|
||
circle(draw, 628, 402, 132, CREAM)
|
||
circle(draw, 396, 622, 132, CREAM)
|
||
circle(draw, 628, 622, 132, CREAM)
|
||
rounded_rectangle(draw, (386, 386, 638, 638), 72, "#121212")
|
||
draw.polygon(regular_polygon(512, 512, 96, 4, math.pi / 4), fill=hex_to_rgb(GOLD))
|
||
circle(draw, 396, 402, 48, CORAL)
|
||
circle(draw, 628, 622, 48, MINT)
|
||
circle(draw, 628, 402, 28, "#121212")
|
||
circle(draw, 396, 622, 28, "#121212")
|
||
|
||
|
||
DRAWERS = {
|
||
"offset_core": draw_offset_core,
|
||
"mold_chip": draw_mold_chip,
|
||
"pinched_tile": draw_pinched_tile,
|
||
"dual_plate": draw_dual_plate,
|
||
"dot_gate": draw_dot_gate,
|
||
"work_knot": draw_work_knot,
|
||
}
|
||
|
||
|
||
def render_variant(style: str) -> Image.Image:
|
||
image = Image.new("RGB", (SIZE * SCALE, SIZE * SCALE), hex_to_rgb("#111111"))
|
||
draw = ImageDraw.Draw(image)
|
||
DRAWERS[style](draw)
|
||
return image.resize((SIZE, SIZE), Image.Resampling.LANCZOS)
|
||
|
||
|
||
def build_svg(style: str) -> str:
|
||
# PNG 是当前评审主物料,SVG 保留为后续设计师重绘的结构草图。
|
||
if style == "offset_core":
|
||
body = f'''
|
||
<rect width="1024" height="1024" rx="160" fill="#111111"/>
|
||
<rect x="236" y="236" width="552" height="552" rx="148" fill="{CREAM}"/>
|
||
<circle cx="610" cy="456" r="116" fill="#111111"/>
|
||
<circle cx="610" cy="456" r="48" fill="{GOLD}"/>
|
||
<rect x="268" y="612" width="268" height="106" rx="53" fill="#111111"/>
|
||
<rect x="294" y="638" width="206" height="52" rx="26" fill="{CREAM}"/>
|
||
<circle cx="352" cy="370" r="34" fill="#111111"/>
|
||
<circle cx="352" cy="370" r="17" fill="{GOLD}"/>'''
|
||
elif style == "mold_chip":
|
||
body = f'''
|
||
<rect width="1024" height="1024" rx="160" fill="#101418"/>
|
||
<path d="M278 230H734L828 330V694L706 794H278L196 708V322Z" fill="{CREAM}"/>
|
||
<circle cx="512" cy="512" r="144" fill="#101418"/>
|
||
<circle cx="512" cy="512" r="62" fill="{GOLD}"/>
|
||
<rect x="224" y="280" width="294" height="90" rx="45" fill="{CORAL}"/>
|
||
<rect x="574" y="654" width="222" height="82" rx="41" fill="{MINT}"/>'''
|
||
elif style == "pinched_tile":
|
||
body = f'''
|
||
<rect width="1024" height="1024" rx="160" fill="#14100d"/>
|
||
<rect x="232" y="250" width="560" height="524" rx="170" fill="{CREAM}"/>
|
||
<circle cx="232" cy="512" r="94" fill="#14100d"/>
|
||
<circle cx="792" cy="512" r="94" fill="#14100d"/>
|
||
<path d="M512 408L616 512L512 616L408 512Z" fill="#14100d"/>
|
||
<circle cx="512" cy="512" r="38" fill="{GOLD}"/>
|
||
<rect x="420" y="300" width="184" height="58" rx="29" fill="{CORAL}"/>
|
||
<rect x="420" y="666" width="184" height="58" rx="29" fill="{MINT}"/>'''
|
||
elif style == "dual_plate":
|
||
body = f'''
|
||
<rect width="1024" height="1024" rx="160" fill="#111111"/>
|
||
<rect x="214" y="316" width="576" height="148" rx="74" fill="{CORAL}"/>
|
||
<rect x="234" y="560" width="576" height="148" rx="74" fill="{MINT}"/>
|
||
<path d="M512 374L650 512L512 650L374 512Z" fill="{CREAM}"/>
|
||
<path d="M512 436L588 512L512 588L436 512Z" fill="#111111"/>
|
||
<circle cx="512" cy="512" r="32" fill="{GOLD}"/>
|
||
<circle cx="262" cy="390" r="24" fill="{CREAM}"/>
|
||
<circle cx="762" cy="634" r="24" fill="{CREAM}"/>'''
|
||
elif style == "dot_gate":
|
||
body = f'''
|
||
<rect width="1024" height="1024" rx="160" fill="#101010"/>
|
||
<rect x="276" y="330" width="472" height="422" rx="132" fill="{CREAM}"/>
|
||
<rect x="386" y="440" width="252" height="312" rx="126" fill="#101010"/>
|
||
<circle cx="512" cy="260" r="62" fill="{GOLD}"/>
|
||
<rect x="450" y="308" width="124" height="196" rx="62" fill="{CREAM}"/>
|
||
<circle cx="512" cy="518" r="40" fill="{GOLD}"/>
|
||
<rect x="316" y="754" width="392" height="58" rx="29" fill="{CREAM}"/>'''
|
||
else:
|
||
body = f'''
|
||
<rect width="1024" height="1024" rx="160" fill="#121212"/>
|
||
<circle cx="396" cy="402" r="132" fill="{CREAM}"/>
|
||
<circle cx="628" cy="402" r="132" fill="{CREAM}"/>
|
||
<circle cx="396" cy="622" r="132" fill="{CREAM}"/>
|
||
<circle cx="628" cy="622" r="132" fill="{CREAM}"/>
|
||
<rect x="386" y="386" width="252" height="252" rx="72" fill="#121212"/>
|
||
<path d="M512 416L608 512L512 608L416 512Z" fill="{GOLD}"/>
|
||
<circle cx="396" cy="402" r="48" fill="{CORAL}"/>
|
||
<circle cx="628" cy="622" r="48" fill="{MINT}"/>
|
||
<circle cx="628" cy="402" r="28" fill="#121212"/>
|
||
<circle cx="396" cy="622" r="28" fill="#121212"/>'''
|
||
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">
|
||
{body}
|
||
</svg>
|
||
'''
|
||
|
||
|
||
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[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, (_, title, 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} {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[str, str, Image.Image]] = []
|
||
|
||
for asset_id, title, style in VARIANTS:
|
||
preview = render_variant(style)
|
||
preview.save(OUTPUT_DIR / f"{asset_id}.png")
|
||
(OUTPUT_DIR / f"{asset_id}.svg").write_text(build_svg(style), encoding="utf-8")
|
||
previews.append((asset_id, title, 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"{asset_id}.png" for asset_id, _, _ in VARIANTS]
|
||
+ [f"{asset_id}.svg" for asset_id, _, _ in VARIANTS]
|
||
+ [CONTACT_SHEET_PATH.name],
|
||
}
|
||
)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|