feat: refresh creation config and visual assets
This commit is contained in:
297
scripts/generate-taonier-abstract-mascot-v2-logo-concepts.py
Normal file
297
scripts/generate-taonier-abstract-mascot-v2-logo-concepts.py
Normal file
@@ -0,0 +1,297 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user