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''' ''' elif style == "mold_chip": body = f''' ''' elif style == "pinched_tile": body = f''' ''' elif style == "dual_plate": body = f''' ''' elif style == "dot_gate": 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()