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'''
'''
elif style == "mold_baby":
body = f'''
'''
elif style == "dot_face":
body = f'''
'''
elif style == "soft_totem":
body = f'''
'''
elif style == "clay_seed":
body = f'''
'''
else:
body = f'''
'''
return f'''
'''
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()