Files
Genarrative/scripts/generate-taonier-abstract-mascot-logo-concepts.py

296 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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-abstract-mascot-concepts"
CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-abstract-mascot-contact-sheet.png"
SIZE = 1024
SCALE = 4
INK = "#121212"
INK_BLUE = "#101418"
CREAM = "#fff5df"
CLAY = "#d77750"
CLAY_DARK = "#b95f3f"
GOLD = "#ffd25d"
MINT = "#31c7a9"
CORAL = "#ff6a5f"
VARIANTS = [
("taonier-abstract-mascot-clay-bean", "陶泥豆偶", "clay_bean"),
("taonier-abstract-mascot-mold-baby", "模胚小灵", "mold_baby"),
("taonier-abstract-mascot-dot-face", "泥点面偶", "dot_face"),
("taonier-abstract-mascot-soft-totem", "软陶图腾", "soft_totem"),
("taonier-abstract-mascot-clay-seed", "陶泥种子", "clay_seed"),
("taonier-abstract-mascot-work-puppet", "作品泥灵", "work_puppet"),
]
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 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 rounded_rect(
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 polygon(cx: float, cy: float, radius: float, sides: int, rotation: float) -> list[tuple[int, int]]:
return [
(s(cx + math.cos(rotation + math.tau * index / sides) * radius), s(cy + math.sin(rotation + math.tau * index / sides) * radius))
for index in range(sides)
]
def star_points(cx: float, cy: float, outer: float, inner: float, count: int = 4) -> list[tuple[int, int]]:
points = []
for index in range(count * 2):
radius = outer if index % 2 == 0 else inner
angle = -math.pi / 2 + index * math.pi / count
points.append((s(cx + math.cos(angle) * radius), s(cy + math.sin(angle) * radius)))
return points
def draw_clay_bean(draw: ImageDraw.ImageDraw) -> None:
draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#101010"))
rounded_rect(draw, (270, 214, 754, 820), 228, CREAM)
circle(draw, 650, 330, 112, CLAY)
circle(draw, 676, 354, 86, CREAM)
circle(draw, 430, 470, 34, INK)
circle(draw, 590, 470, 34, INK)
draw.polygon(star_points(512, 618, 66, 28), fill=hex_to_rgb(GOLD))
rounded_rect(draw, (360, 742, 664, 790), 24, CLAY)
def draw_mold_baby(draw: ImageDraw.ImageDraw) -> None:
draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb(INK_BLUE))
draw.polygon(polygon(512, 498, 304, 8, math.pi / 8), fill=hex_to_rgb(CREAM))
circle(draw, 512, 398, 126, "#101418")
circle(draw, 512, 398, 62, GOLD)
circle(draw, 390, 570, 30, "#101418")
circle(draw, 634, 570, 30, "#101418")
rounded_rect(draw, (380, 704, 644, 758), 27, MINT)
rounded_rect(draw, (318, 268, 460, 326), 29, CORAL)
def draw_dot_face(draw: ImageDraw.ImageDraw) -> None:
draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#17110e"))
rounded_rect(draw, (254, 254, 770, 770), 190, CLAY)
rounded_rect(draw, (330, 320, 694, 706), 140, CREAM)
circle(draw, 440, 478, 30, "#17110e")
circle(draw, 584, 478, 30, "#17110e")
rounded_rect(draw, (458, 594, 566, 638), 22, CLAY)
circle(draw, 512, 254, 54, GOLD)
circle(draw, 512, 254, 24, "#17110e")
def draw_soft_totem(draw: ImageDraw.ImageDraw) -> None:
draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#111111"))
rounded_rect(draw, (340, 188, 684, 836), 172, CREAM)
circle(draw, 512, 336, 112, CLAY)
rounded_rect(draw, (390, 442, 634, 722), 122, "#111111")
circle(draw, 512, 582, 58, GOLD)
circle(draw, 432, 332, 24, INK)
circle(draw, 592, 332, 24, INK)
rounded_rect(draw, (404, 782, 620, 842), 30, CREAM)
def draw_clay_seed(draw: ImageDraw.ImageDraw) -> None:
draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#101418"))
draw.pieslice((s(236), s(176), s(788), s(832)), start=218, end=578, fill=hex_to_rgb(CREAM))
circle(draw, 618, 326, 72, "#101418")
circle(draw, 618, 326, 34, GOLD)
circle(draw, 438, 488, 30, "#101418")
rounded_rect(draw, (506, 548, 632, 594), 23, CLAY)
draw.polygon(star_points(512, 682, 58, 24), fill=hex_to_rgb(GOLD))
def draw_work_puppet(draw: ImageDraw.ImageDraw) -> None:
draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#101010"))
rounded_rect(draw, (286, 300, 738, 756), 150, CREAM)
circle(draw, 286, 530, 88, "#101010")
circle(draw, 738, 530, 88, "#101010")
circle(draw, 512, 300, 76, GOLD)
circle(draw, 430, 474, 24, "#101010")
circle(draw, 594, 474, 24, "#101010")
draw.polygon(star_points(512, 604, 68, 28), fill=hex_to_rgb("#101010"))
draw.polygon(star_points(512, 604, 34, 14), fill=hex_to_rgb(GOLD))
rounded_rect(draw, (360, 744, 664, 798), 27, CLAY)
DRAWERS = {
"clay_bean": draw_clay_bean,
"mold_baby": draw_mold_baby,
"dot_face": draw_dot_face,
"soft_totem": draw_soft_totem,
"clay_seed": draw_clay_seed,
"work_puppet": draw_work_puppet,
}
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 == "clay_bean":
body = f'''
<rect width="1024" height="1024" rx="160" fill="#101010"/>
<rect x="270" y="214" width="484" height="606" rx="228" fill="{CREAM}"/>
<circle cx="650" cy="330" r="112" fill="{CLAY}"/>
<circle cx="676" cy="354" r="86" fill="{CREAM}"/>
<circle cx="430" cy="470" r="34" fill="{INK}"/>
<circle cx="590" cy="470" r="34" fill="{INK}"/>
<path d="M512 552L540 590L578 618L540 646L512 684L484 646L446 618L484 590Z" fill="{GOLD}"/>
<rect x="360" y="742" width="304" height="48" rx="24" fill="{CLAY}"/>'''
elif style == "mold_baby":
body = f'''
<rect width="1024" height="1024" rx="160" fill="{INK_BLUE}"/>
<path d="M628 217L759 272L814 402L759 724L628 779H396L265 724L210 402L265 272L396 217Z" fill="{CREAM}"/>
<circle cx="512" cy="398" r="126" fill="{INK_BLUE}"/>
<circle cx="512" cy="398" r="62" fill="{GOLD}"/>
<circle cx="390" cy="570" r="30" fill="{INK_BLUE}"/>
<circle cx="634" cy="570" r="30" fill="{INK_BLUE}"/>
<rect x="380" y="704" width="264" height="54" rx="27" fill="{MINT}"/>
<rect x="318" y="268" width="142" height="58" rx="29" fill="{CORAL}"/>'''
elif style == "dot_face":
body = f'''
<rect width="1024" height="1024" rx="160" fill="#17110e"/>
<rect x="254" y="254" width="516" height="516" rx="190" fill="{CLAY}"/>
<rect x="330" y="320" width="364" height="386" rx="140" fill="{CREAM}"/>
<circle cx="440" cy="478" r="30" fill="#17110e"/>
<circle cx="584" cy="478" r="30" fill="#17110e"/>
<rect x="458" y="594" width="108" height="44" rx="22" fill="{CLAY}"/>
<circle cx="512" cy="254" r="54" fill="{GOLD}"/>
<circle cx="512" cy="254" r="24" fill="#17110e"/>'''
elif style == "soft_totem":
body = f'''
<rect width="1024" height="1024" rx="160" fill="#111111"/>
<rect x="340" y="188" width="344" height="648" rx="172" fill="{CREAM}"/>
<circle cx="512" cy="336" r="112" fill="{CLAY}"/>
<rect x="390" y="442" width="244" height="280" rx="122" fill="#111111"/>
<circle cx="512" cy="582" r="58" fill="{GOLD}"/>
<circle cx="432" cy="332" r="24" fill="{INK}"/>
<circle cx="592" cy="332" r="24" fill="{INK}"/>
<rect x="404" y="782" width="216" height="60" rx="30" fill="{CREAM}"/>'''
elif style == "clay_seed":
body = f'''
<rect width="1024" height="1024" rx="160" fill="{INK_BLUE}"/>
<path d="M236 504C236 294 382 176 512 176C676 176 788 322 788 504C788 702 648 832 512 832C372 832 236 702 236 504Z" fill="{CREAM}"/>
<circle cx="618" cy="326" r="72" fill="{INK_BLUE}"/>
<circle cx="618" cy="326" r="34" fill="{GOLD}"/>
<circle cx="438" cy="488" r="30" fill="{INK_BLUE}"/>
<rect x="506" y="548" width="126" height="46" rx="23" fill="{CLAY}"/>
<path d="M512 624L536 658L570 682L536 706L512 740L488 706L454 682L488 658Z" fill="{GOLD}"/>'''
else:
body = f'''
<rect width="1024" height="1024" rx="160" fill="#101010"/>
<rect x="286" y="300" width="452" height="456" rx="150" fill="{CREAM}"/>
<circle cx="286" cy="530" r="88" fill="#101010"/>
<circle cx="738" cy="530" r="88" fill="#101010"/>
<circle cx="512" cy="300" r="76" fill="{GOLD}"/>
<circle cx="430" cy="474" r="24" fill="#101010"/>
<circle cx="594" cy="474" r="24" fill="#101010"/>
<path d="M512 536L540 576L580 604L540 632L512 672L484 632L444 604L484 576Z" fill="#101010"/>
<path d="M512 570L526 590L546 604L526 618L512 638L498 618L478 604L498 590Z" fill="{GOLD}"/>
<rect x="360" y="744" width="304" height="54" rx="27" fill="{CLAY}"/>'''
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()