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-v2-concepts"
CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-abstract-mascot-v2-contact-sheet.png"
SIZE = 1024
SCALE = 4
BG = "#101010"
BG_WARM = "#17110e"
BG_BLUE = "#101418"
INK = "#111111"
CREAM = "#fff3d7"
CLAY = "#df7650"
CLAY_DARK = "#bd5b3d"
GOLD = "#ffd35f"
MINT = "#2ec5ad"
CORAL = "#ff6b61"
VARIANTS = [
("taonier-abstract-mascot-v2-clay-sprite", "陶泥小灵", "clay_sprite"),
("taonier-abstract-mascot-v2-pinch-orbit", "捏孔泥偶", "pinch_orbit"),
("taonier-abstract-mascot-v2-seed-totem", "星胚图腾", "seed_totem"),
("taonier-abstract-mascot-v2-soft-mold", "软模团子", "soft_mold"),
("taonier-abstract-mascot-v2-clay-orb", "泥芯圆偶", "clay_orb"),
("taonier-abstract-mascot-v2-work-glyph", "作品泥符", "work_glyph"),
]
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 ellipse(
draw: ImageDraw.ImageDraw,
box: tuple[float, float, float, float],
fill: str,
) -> None:
draw.ellipse(tuple(s(value) for value in box), 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 sparkle(cx: float, cy: float, outer: float, inner: float) -> list[tuple[int, int]]:
points = []
for index in range(8):
radius = outer if index % 2 == 0 else inner
angle = -math.pi / 2 + index * math.pi / 4
points.append((s(cx + math.cos(angle) * radius), s(cy + math.sin(angle) * radius)))
return points
def draw_clay_sprite(draw: ImageDraw.ImageDraw) -> None:
draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb(BG))
rounded_rect(draw, (308, 210, 708, 810), 198, CREAM)
circle(draw, 672, 302, 82, CLAY)
circle(draw, 716, 336, 74, BG)
circle(draw, 402, 458, 34, INK)
draw.polygon(sparkle(548, 584, 64, 24), fill=hex_to_rgb(GOLD))
rounded_rect(draw, (362, 742, 660, 792), 25, CLAY)
def draw_pinch_orbit(draw: ImageDraw.ImageDraw) -> None:
draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb(BG_BLUE))
circle(draw, 512, 512, 276, CREAM)
circle(draw, 724, 394, 94, BG_BLUE)
circle(draw, 692, 412, 38, GOLD)
circle(draw, 314, 512, 76, BG_BLUE)
rounded_rect(draw, (442, 642, 594, 690), 24, CLAY)
circle(draw, 438, 448, 32, INK)
def draw_seed_totem(draw: ImageDraw.ImageDraw) -> None:
draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb(BG_WARM))
draw.polygon(polygon(512, 514, 310, 8, math.pi / 8), fill=hex_to_rgb(CREAM))
circle(draw, 512, 282, 76, GOLD)
rounded_rect(draw, (376, 356, 648, 726), 136, CLAY)
circle(draw, 430, 528, 28, BG_WARM)
circle(draw, 594, 528, 28, BG_WARM)
draw.polygon(sparkle(512, 634, 58, 22), fill=hex_to_rgb(CREAM))
rounded_rect(draw, (386, 764, 638, 818), 27, MINT)
def draw_soft_mold(draw: ImageDraw.ImageDraw) -> None:
draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb(BG))
rounded_rect(draw, (270, 300, 754, 740), 148, CREAM)
circle(draw, 270, 520, 84, BG)
circle(draw, 754, 520, 84, BG)
rounded_rect(draw, (390, 404, 634, 650), 110, CLAY)
circle(draw, 512, 526, 62, GOLD)
circle(draw, 512, 526, 28, BG)
rounded_rect(draw, (356, 728, 668, 782), 27, CLAY_DARK)
def draw_clay_orb(draw: ImageDraw.ImageDraw) -> None:
draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb(BG_BLUE))
circle(draw, 512, 512, 274, CREAM)
circle(draw, 512, 512, 142, BG_BLUE)
draw.polygon(sparkle(512, 512, 70, 26), fill=hex_to_rgb(GOLD))
circle(draw, 648, 340, 64, CLAY)
circle(draw, 666, 360, 40, BG_BLUE)
circle(draw, 374, 650, 46, MINT)
def draw_work_glyph(draw: ImageDraw.ImageDraw) -> None:
draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb(BG_WARM))
rounded_rect(draw, (340, 184, 684, 832), 172, CREAM)
circle(draw, 512, 348, 124, CLAY)
circle(draw, 512, 348, 56, BG_WARM)
draw.polygon(sparkle(512, 348, 42, 15), fill=hex_to_rgb(GOLD))
rounded_rect(draw, (412, 492, 612, 690), 98, BG_WARM)
circle(draw, 512, 592, 50, GOLD)
rounded_rect(draw, (404, 764, 620, 824), 30, CREAM)
DRAWERS = {
"clay_sprite": draw_clay_sprite,
"pinch_orbit": draw_pinch_orbit,
"seed_totem": draw_seed_totem,
"soft_mold": draw_soft_mold,
"clay_orb": draw_clay_orb,
"work_glyph": draw_work_glyph,
}
def render_variant(style: str) -> Image.Image:
image = Image.new("RGB", (SIZE * SCALE, SIZE * SCALE), hex_to_rgb(BG))
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_sprite":
body = f'''
'''
elif style == "pinch_orbit":
body = f'''
'''
elif style == "seed_totem":
body = f'''
'''
elif style == "soft_mold":
body = f'''
'''
elif style == "clay_orb":
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()