From ca6f9be3818641e87d854a82646d2ea2aff6e545 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 12 Jun 2026 12:52:43 +0800
Subject: [PATCH] Add Quick Switch view mode entry
---
C3.tools/generate-app-icon.py | 269 +++++++++++------------------------------------------
1 files changed, 55 insertions(+), 214 deletions(-)
diff --git a/C3.tools/generate-app-icon.py b/C3.tools/generate-app-icon.py
index bb01b28..d75bc57 100755
--- a/C3.tools/generate-app-icon.py
+++ b/C3.tools/generate-app-icon.py
@@ -1,241 +1,82 @@
#!/usr/bin/env python3
-"""Generate Aligner's macOS app icon assets."""
+"""Sync Aligner's macOS app icon assets from the design handoff."""
from __future__ import annotations
import json
import shutil
-import subprocess
from pathlib import Path
-from PIL import Image, ImageDraw, ImageFilter
-
-ROOT = Path(__file__).resolve().parents[1]
-RESOURCES = ROOT / "C1.source" / "Resources"
+OUTPUT_ROOT = Path(__file__).resolve().parents[1]
+PROJECT_ROOT = OUTPUT_ROOT.parent
+RESOURCES = OUTPUT_ROOT / "C1.source" / "Resources"
APPICONSET = RESOURCES / "Assets.xcassets" / "AppIcon.appiconset"
ICNS_PATH = RESOURCES / "AppIcon.icns"
-BASE_SIZE = 1024
-SCALE = 4
-CANVAS_SIZE = BASE_SIZE * SCALE
+HANDOFF_DIR = (
+ PROJECT_ROOT
+ / "02-P"
+ / "Round01-QuickSwitch-MVP"
+ / "02.ui设计稿"
+ / "AppIcon-D1-BigBars-Handoff-20260604"
+)
+HANDOFF_ICONSET = HANDOFF_DIR / "AppIcon-D1-BigBars.iconset"
+HANDOFF_ICNS = HANDOFF_DIR / "Aligner-AppIcon-D1-BigBars.icns"
+HANDOFF_PREVIEW = HANDOFF_DIR / "Aligner-AppIcon-D1-BigBars-1024.png"
+
+ICON_FILES = [
+ "icon_16x16.png",
+ "icon_16x16@2x.png",
+ "icon_32x32.png",
+ "icon_32x32@2x.png",
+ "icon_128x128.png",
+ "icon_128x128@2x.png",
+ "icon_256x256.png",
+ "icon_256x256@2x.png",
+ "icon_512x512.png",
+ "icon_512x512@2x.png",
+]
-def s(value: float) -> int:
- return round(value * SCALE)
+def require_file(path: Path) -> None:
+ if not path.is_file():
+ raise FileNotFoundError(f"missing app icon source: {path}")
-def sb(bbox: tuple[float, float, float, float]) -> tuple[int, int, int, int]:
- return tuple(s(value) for value in bbox)
+def load_contents_json() -> dict:
+ contents_path = HANDOFF_ICONSET / "Contents.json"
+ require_file(contents_path)
+ with contents_path.open("r", encoding="utf-8") as file:
+ return json.load(file)
-def rounded_mask(size: tuple[int, int], bbox: tuple[int, int, int, int], radius: int) -> Image.Image:
- mask = Image.new("L", size, 0)
- ImageDraw.Draw(mask).rounded_rectangle(bbox, radius=radius, fill=255)
- return mask
-
-
-def composite_masked(base: Image.Image, layer: Image.Image, mask: Image.Image) -> None:
- base.alpha_composite(Image.composite(layer, Image.new("RGBA", base.size), mask))
-
-
-def vertical_gradient(
- size: tuple[int, int],
- top: tuple[int, int, int, int],
- bottom: tuple[int, int, int, int],
-) -> Image.Image:
- image = Image.new("RGBA", size)
- draw = ImageDraw.Draw(image)
- height = max(size[1] - 1, 1)
- for y in range(size[1]):
- t = y / height
- color = tuple(round(top[i] * (1 - t) + bottom[i] * t) for i in range(4))
- draw.line((0, y, size[0], y), fill=color)
- return image
-
-
-def add_shadow(
- base: Image.Image,
- mask: Image.Image,
- color: tuple[int, int, int, int],
- blur: int,
- dx: int,
- dy: int,
-) -> None:
- shadow_mask = mask.filter(ImageFilter.GaussianBlur(blur))
- shifted = Image.new("L", base.size, 0)
- shifted.paste(shadow_mask, (dx, dy))
- composite_masked(base, Image.new("RGBA", base.size, color), shifted)
-
-
-def draw_outer_shell(canvas: Image.Image) -> None:
- bbox = sb((78, 58, 946, 934))
- radius = s(206)
- mask = rounded_mask(canvas.size, bbox, radius)
-
- add_shadow(canvas, mask, (55, 72, 98, 22), s(16), 0, s(16))
- add_shadow(canvas, mask, (255, 255, 255, 135), s(8), 0, s(-5))
-
- shell_fill = vertical_gradient(
- canvas.size,
- (253, 254, 255, 250),
- (228, 236, 246, 247),
- )
- composite_masked(canvas, shell_fill, mask)
-
- edge = ImageDraw.Draw(canvas)
- edge.rounded_rectangle(bbox, radius=radius, outline=(255, 255, 255, 226), width=s(7))
- edge.rounded_rectangle(
- sb((90, 70, 934, 922)),
- radius=radius - s(12),
- outline=(255, 255, 255, 85),
- width=s(2),
- )
- edge.rounded_rectangle(bbox, radius=radius, outline=(168, 184, 206, 32), width=s(2))
-
-
-def draw_capsule(
- canvas: Image.Image,
- bbox: tuple[int, int, int, int],
- top: tuple[int, int, int, int],
- bottom: tuple[int, int, int, int],
- outer_edge: tuple[int, int, int, int],
- shadow_color: tuple[int, int, int, int],
- shadow_blur: int,
- is_accent: bool = False,
-) -> None:
- radius = (bbox[2] - bbox[0]) // 2
- mask = rounded_mask(canvas.size, bbox, radius)
-
- add_shadow(canvas, mask, shadow_color, shadow_blur, 0, s(16 if not is_accent else 18))
- fill = vertical_gradient(canvas.size, top, bottom)
- composite_masked(canvas, fill, mask)
-
- draw = ImageDraw.Draw(canvas)
- draw.rounded_rectangle(bbox, radius=radius, outline=(255, 255, 255, 218), width=s(6))
- draw.rounded_rectangle(
- (bbox[0] + s(8), bbox[1] + s(8), bbox[2] - s(8), bbox[3] - s(8)),
- radius=radius - s(9),
- outline=(255, 255, 255, 102 if not is_accent else 76),
- width=s(2),
- )
- draw.rounded_rectangle(bbox, radius=radius, outline=outer_edge, width=s(2))
-
- if not is_accent:
- shade = Image.new("RGBA", canvas.size, (0, 0, 0, 0))
- shade_draw = ImageDraw.Draw(shade)
- shade_draw.rounded_rectangle(
- (bbox[0] + s(7), bbox[1] + s(12), bbox[0] + s(30), bbox[3] - s(12)),
- radius=radius // 2,
- fill=(136, 158, 184, 22),
- )
- composite_masked(canvas, shade, mask)
-
-
-def draw_icon() -> Image.Image:
- canvas = Image.new("RGBA", (CANVAS_SIZE, CANVAS_SIZE), (0, 0, 0, 0))
- draw_outer_shell(canvas)
-
- left_bar = sb((300, 262, 420, 742))
- middle_bar = sb((452, 262, 572, 742))
- accent_bar = sb((604, 262, 724, 742))
-
- neutral_edge = (136, 158, 184, 58)
- draw_capsule(
- canvas,
- left_bar,
- (238, 245, 252, 226),
- (228, 237, 247, 226),
- neutral_edge,
- (136, 158, 184, 58),
- s(36),
- )
- draw_capsule(
- canvas,
- middle_bar,
- (232, 241, 249, 232),
- (222, 234, 246, 232),
- neutral_edge,
- (136, 158, 184, 66),
- s(38),
- )
- draw_capsule(
- canvas,
- accent_bar,
- (75, 183, 255, 238),
- (20, 145, 255, 238),
- (28, 133, 255, 72),
- (25, 118, 210, 62),
- s(20),
- is_accent=True,
- )
-
- return canvas.resize((BASE_SIZE, BASE_SIZE), Image.Resampling.LANCZOS)
-
-
-def write_contents_json() -> None:
- entries = [
- ("16x16", "1x", "icon_16x16.png"),
- ("16x16", "2x", "icon_16x16@2x.png"),
- ("32x32", "1x", "icon_32x32.png"),
- ("32x32", "2x", "icon_32x32@2x.png"),
- ("128x128", "1x", "icon_128x128.png"),
- ("128x128", "2x", "icon_128x128@2x.png"),
- ("256x256", "1x", "icon_256x256.png"),
- ("256x256", "2x", "icon_256x256@2x.png"),
- ("512x512", "1x", "icon_512x512.png"),
- ("512x512", "2x", "icon_512x512@2x.png"),
- ]
- payload = {
- "images": [
- {"filename": filename, "idiom": "mac", "scale": scale, "size": size}
- for size, scale, filename in entries
- ],
- "info": {"author": "xcode", "version": 1},
- }
- (APPICONSET / "Contents.json").write_text(json.dumps(payload, indent=2) + "\n")
-
-
-def write_icon_family(source: Image.Image) -> None:
- sizes = {
- "icon_16x16.png": 16,
- "icon_16x16@2x.png": 32,
- "icon_32x32.png": 32,
- "icon_32x32@2x.png": 64,
- "icon_128x128.png": 128,
- "icon_128x128@2x.png": 256,
- "icon_256x256.png": 256,
- "icon_256x256@2x.png": 512,
- "icon_512x512.png": 512,
- "icon_512x512@2x.png": 1024,
- }
-
+def sync_iconset() -> None:
APPICONSET.mkdir(parents=True, exist_ok=True)
- source.save(APPICONSET / "icon_1024_preview.png")
- for filename, size in sizes.items():
- source.resize((size, size), Image.Resampling.LANCZOS).save(APPICONSET / filename)
+ contents = load_contents_json()
+
+ for filename in ICON_FILES:
+ source = HANDOFF_ICONSET / filename
+ require_file(source)
+ shutil.copy2(source, APPICONSET / filename)
+
+ require_file(HANDOFF_PREVIEW)
+ shutil.copy2(HANDOFF_PREVIEW, APPICONSET / "icon_1024_preview.png")
+
+ with (APPICONSET / "Contents.json").open("w", encoding="utf-8") as file:
+ json.dump(contents, file, indent=2, ensure_ascii=False)
+ file.write("\n")
-def write_icns() -> None:
- iconset = RESOURCES / "AppIcon.iconset"
- if iconset.exists():
- shutil.rmtree(iconset)
- iconset.mkdir()
-
- for icon in APPICONSET.glob("icon_*.png"):
- if icon.name == "icon_1024_preview.png":
- continue
- shutil.copy2(icon, iconset / icon.name)
-
- subprocess.run(["iconutil", "-c", "icns", str(iconset), "-o", str(ICNS_PATH)], check=True)
- shutil.rmtree(iconset)
+def sync_icns() -> None:
+ require_file(HANDOFF_ICNS)
+ shutil.copy2(HANDOFF_ICNS, ICNS_PATH)
def main() -> None:
- icon = draw_icon()
- write_icon_family(icon)
- write_contents_json()
- write_icns()
+ sync_iconset()
+ sync_icns()
+ print(f"Synced AppIcon assets from: {HANDOFF_DIR}")
if __name__ == "__main__":
--
Gitblit v1.9.3