Files
Genarrative/scripts/generate-taonier-abstract-mascot-v2-logo-concepts.py

298 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'''
<rect width="1024" height="1024" rx="160" fill="{BG}"/>
<rect x="308" y="210" width="400" height="600" rx="198" fill="{CREAM}"/>
<circle cx="672" cy="302" r="82" fill="{CLAY}"/>
<circle cx="716" cy="336" r="74" fill="{BG}"/>
<circle cx="402" cy="458" r="34" fill="{INK}"/>
<path d="M548 520L572 560L612 584L572 608L548 648L524 608L484 584L524 560Z" fill="{GOLD}"/>
<rect x="362" y="742" width="298" height="50" rx="25" fill="{CLAY}"/>'''
elif style == "pinch_orbit":
body = f'''
<rect width="1024" height="1024" rx="160" fill="{BG_BLUE}"/>
<circle cx="512" cy="512" r="276" fill="{CREAM}"/>
<circle cx="724" cy="394" r="94" fill="{BG_BLUE}"/>
<circle cx="692" cy="412" r="38" fill="{GOLD}"/>
<circle cx="314" cy="512" r="76" fill="{BG_BLUE}"/>
<rect x="442" y="642" width="152" height="48" rx="24" fill="{CLAY}"/>
<circle cx="438" cy="448" r="32" fill="{INK}"/>'''
elif style == "seed_totem":
body = f'''
<rect width="1024" height="1024" rx="160" fill="{BG_WARM}"/>
<path d="M630 228L796 394V630L630 796H394L228 630V394L394 228Z" fill="{CREAM}"/>
<circle cx="512" cy="282" r="76" fill="{GOLD}"/>
<rect x="376" y="356" width="272" height="370" rx="136" fill="{CLAY}"/>
<circle cx="430" cy="528" r="28" fill="{BG_WARM}"/>
<circle cx="594" cy="528" r="28" fill="{BG_WARM}"/>
<path d="M512 576L534 612L570 634L534 656L512 692L490 656L454 634L490 612Z" fill="{CREAM}"/>
<rect x="386" y="764" width="252" height="54" rx="27" fill="{MINT}"/>'''
elif style == "soft_mold":
body = f'''
<rect width="1024" height="1024" rx="160" fill="{BG}"/>
<rect x="270" y="300" width="484" height="440" rx="148" fill="{CREAM}"/>
<circle cx="270" cy="520" r="84" fill="{BG}"/>
<circle cx="754" cy="520" r="84" fill="{BG}"/>
<rect x="390" y="404" width="244" height="246" rx="110" fill="{CLAY}"/>
<circle cx="512" cy="526" r="62" fill="{GOLD}"/>
<circle cx="512" cy="526" r="28" fill="{BG}"/>
<rect x="356" y="728" width="312" height="54" rx="27" fill="{CLAY_DARK}"/>'''
elif style == "clay_orb":
body = f'''
<rect width="1024" height="1024" rx="160" fill="{BG_BLUE}"/>
<circle cx="512" cy="512" r="274" fill="{CREAM}"/>
<circle cx="512" cy="512" r="142" fill="{BG_BLUE}"/>
<path d="M512 442L538 486L582 512L538 538L512 582L486 538L442 512L486 486Z" fill="{GOLD}"/>
<circle cx="648" cy="340" r="64" fill="{CLAY}"/>
<circle cx="666" cy="360" r="40" fill="{BG_BLUE}"/>
<circle cx="374" cy="650" r="46" fill="{MINT}"/>'''
else:
body = f'''
<rect width="1024" height="1024" rx="160" fill="{BG_WARM}"/>
<rect x="340" y="184" width="344" height="648" rx="172" fill="{CREAM}"/>
<circle cx="512" cy="348" r="124" fill="{CLAY}"/>
<circle cx="512" cy="348" r="56" fill="{BG_WARM}"/>
<path d="M512 306L527 333L554 348L527 363L512 390L497 363L470 348L497 333Z" fill="{GOLD}"/>
<rect x="412" y="492" width="200" height="198" rx="98" fill="{BG_WARM}"/>
<circle cx="512" cy="592" r="50" fill="{GOLD}"/>
<rect x="404" y="764" width="216" height="60" rx="30" fill="{CREAM}"/>'''
return f'''<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
{body}
</svg>
'''
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()