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''' {body} ''' 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()