from __future__ import annotations from pathlib import Path from typing import Iterable from PIL import Image, ImageDraw, ImageFont REPO_ROOT = Path(__file__).resolve().parents[1] OUTPUT_DIR = REPO_ROOT / "public" / "branding" / "taonier-logo-anchor-concepts" CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-anchor-contact-sheet.png" SIZE = 1024 SCALE = 4 VARIANTS = [ { "id": "taonier-anchor-core", "title": "泥点锚标", "bg": "#151515", "mark": "#ffffff", "accent": "#ffffff", "style": "core", }, { "id": "taonier-anchor-soft-slab", "title": "软泥层台", "bg": "#111111", "mark": "#fffdf4", "accent": "#fffdf4", "style": "soft_slab", }, { "id": "taonier-anchor-work-stack", "title": "作品叠层", "bg": "#171717", "mark": "#ffffff", "accent": "#ffffff", "style": "work_stack", }, { "id": "taonier-anchor-clay-drop", "title": "泥点落印", "bg": "#151515", "mark": "#ffffff", "accent": "#f5c95d", "style": "clay_drop", }, { "id": "taonier-anchor-creation-base", "title": "创作底座", "bg": "#121212", "mark": "#ffffff", "accent": "#ffffff", "style": "creation_base", }, { "id": "taonier-anchor-app-token", "title": "泥点应用标", "bg": "#101418", "mark": "#fffaf0", "accent": "#ffd45d", "style": "app_token", }, ] 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 scaled_points(points: Iterable[tuple[float, float]]) -> list[tuple[int, int]]: return [(s(x), s(y)) for x, y in points] def quad( start: tuple[float, float], control: tuple[float, float], end: tuple[float, float], steps: int = 24, ) -> list[tuple[float, float]]: points: list[tuple[float, float]] = [] for index in range(steps + 1): t = index / steps x = (1 - t) * (1 - t) * start[0] + 2 * (1 - t) * t * control[0] + t * t * end[0] y = (1 - t) * (1 - t) * start[1] + 2 * (1 - t) * t * control[1] + t * t * end[1] points.append((x, y)) return points def round_line( draw: ImageDraw.ImageDraw, points: list[tuple[float, float]], fill: str, width: int, closed: bool = False, ) -> None: scaled = scaled_points(points) if closed: scaled = [*scaled, scaled[0]] draw.line(scaled, fill=hex_to_rgb(fill), width=s(width), joint="curve") radius = s(width) // 2 if not closed: for x, y in (scaled[0], scaled[-1]): draw.ellipse((x - radius, y - radius, x + radius, y + radius), fill=hex_to_rgb(fill)) def round_circle(draw: ImageDraw.ImageDraw, center: tuple[float, float], radius: float, fill: str) -> None: x, y = center draw.ellipse((s(x - radius), s(y - radius), s(x + radius), s(y + radius)), fill=hex_to_rgb(fill)) def draw_core(draw: ImageDraw.ImageDraw, mark: str, accent: str) -> None: round_line(draw, [(512, 326), (512, 514)], mark, 68) round_circle(draw, (512, 214), 62, accent) round_line(draw, [(244, 548), (512, 430), (780, 548), (512, 674)], mark, 66, closed=True) round_line(draw, [(292, 656), (468, 734), (512, 752), (556, 734), (732, 656)], mark, 52) round_circle(draw, (337, 548), 17, mark) def draw_soft_slab(draw: ImageDraw.ImageDraw, mark: str, accent: str) -> None: round_line(draw, [(512, 316), (512, 518)], mark, 72) round_circle(draw, (512, 205), 58, accent) top = ( quad((232, 560), (512, 410), (792, 560), 32) + quad((792, 560), (816, 576), (792, 592), 8)[1:] + quad((792, 592), (610, 648), (552, 692), 20)[1:] + quad((552, 692), (512, 716), (472, 692), 12)[1:] + quad((472, 692), (414, 648), (232, 592), 20)[1:] + quad((232, 592), (208, 576), (232, 560), 8)[1:] ) round_line(draw, top, mark, 56, closed=True) round_line(draw, [(278, 642), (470, 728), (512, 748), (554, 728), (746, 642)], mark, 46) round_circle(draw, (342, 554), 15, mark) def draw_work_stack(draw: ImageDraw.ImageDraw, mark: str, accent: str) -> None: round_line(draw, [(512, 310), (512, 504)], mark, 64) round_circle(draw, (512, 205), 56, accent) round_line(draw, [(246, 532), (512, 420), (778, 532), (512, 650)], mark, 58, closed=True) round_line(draw, [(286, 628), (472, 710), (512, 728), (552, 710), (738, 628)], mark, 48) round_line(draw, [(330, 708), (478, 774), (512, 790), (546, 774), (694, 708)], mark, 38) round_circle(draw, (348, 535), 14, mark) def draw_clay_drop(draw: ImageDraw.ImageDraw, mark: str, accent: str) -> None: round_line(draw, [(512, 334), (512, 504)], mark, 64) round_circle(draw, (512, 204), 62, accent) round_line(draw, [(252, 548), (512, 436), (772, 548), (512, 666)], mark, 62, closed=True) round_line(draw, [(304, 642), (474, 718), (512, 736), (550, 718), (720, 642)], mark, 50) round_circle(draw, (346, 548), 16, mark) round_circle(draw, (512, 556), 12, accent) def draw_creation_base(draw: ImageDraw.ImageDraw, mark: str, accent: str) -> None: round_line(draw, [(512, 304), (512, 494)], mark, 58) round_circle(draw, (512, 202), 55, accent) round_line(draw, [(276, 522), (512, 420), (748, 522)], mark, 54) round_line(draw, [(236, 586), (512, 708), (788, 586)], mark, 60) round_line(draw, [(292, 676), (478, 756), (512, 770), (546, 756), (732, 676)], mark, 42) round_circle(draw, (355, 544), 13, mark) def draw_app_token(draw: ImageDraw.ImageDraw, mark: str, accent: str) -> None: round_line(draw, [(512, 326), (512, 508)], mark, 70) round_circle(draw, (512, 208), 62, accent) round_line(draw, [(252, 548), (512, 432), (772, 548), (512, 674)], mark, 66, closed=True) round_line(draw, [(296, 656), (470, 732), (512, 752), (554, 732), (728, 656)], mark, 52) round_circle(draw, (338, 548), 15, accent) DRAWERS = { "core": draw_core, "soft_slab": draw_soft_slab, "work_stack": draw_work_stack, "clay_drop": draw_clay_drop, "creation_base": draw_creation_base, "app_token": draw_app_token, } def build_svg(variant: dict[str, str]) -> str: bg = variant["bg"] mark = variant["mark"] accent = variant["accent"] style = variant["style"] shared = 'fill="none" stroke-linecap="round" stroke-linejoin="round"' dot_fill = accent if style in {"clay_drop", "app_token"} else mark left_dot_fill = accent if style == "app_token" else mark if style == "soft_slab": base = f''' ''' stem = f'' dot = f'' elif style == "work_stack": base = f''' ''' stem = f'' dot = f'' elif style == "clay_drop": base = f''' ''' stem = f'' dot = f'' elif style == "creation_base": base = f''' ''' stem = f'' dot = f'' else: stroke = 70 if style == "app_token" else 68 base_width = 66 layer_width = 52 base = f''' ''' stem = f'' dot = f'' return f''' {base} {stem} {dot} ''' def render_variant(variant: dict[str, str]) -> Image.Image: image = Image.new("RGB", (SIZE * SCALE, SIZE * SCALE), hex_to_rgb(variant["bg"])) draw = ImageDraw.Draw(image) DRAWERS[variant["style"]](draw, variant["mark"], variant["accent"]) return image.resize((SIZE, SIZE), Image.Resampling.LANCZOS) 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[dict[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, (variant, 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} {variant['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[dict[str, str], Image.Image]] = [] for variant in VARIANTS: (OUTPUT_DIR / f"{variant['id']}.svg").write_text(build_svg(variant), encoding="utf-8") preview = render_variant(variant) preview.save(OUTPUT_DIR / f"{variant['id']}.png") previews.append((variant, 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"{variant['id']}.svg" for variant in VARIANTS] + [f"{variant['id']}.png" for variant in VARIANTS] + [CONTACT_SHEET_PATH.name], } ) if __name__ == "__main__": main()