feat: generate app icon (bug beetle design)

Python PIL script generates a bug beetle icon with:
- Dark charcoal body with lighter shell highlight
- Green accent stripe across the wing casing
- Head with white eyes, two curved antennae with dot tips
- Six legs (3 per side)
- All 10 required macOS sizes (16..512 at @1x/@2x)

Run: python3 scripts/generate_app_icon.py

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
tigerenwork 2026-06-29 23:26:23 +08:00
parent f7682ae7cf
commit 20824443cc
13 changed files with 239 additions and 35 deletions

View File

@ -22,6 +22,13 @@
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
</BuildAction> </BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
@ -43,4 +50,18 @@
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
</LaunchAction> </LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme> </Scheme>

View File

@ -1,58 +1,68 @@
{ {
"images" : [ "images": [
{ {
"idiom" : "mac", "size": "16x16",
"scale" : "1x", "idiom": "mac",
"size" : "16x16" "filename": "icon_16x16@x1.png",
"scale": "1x"
}, },
{ {
"idiom" : "mac", "size": "16x16",
"scale" : "2x", "idiom": "mac",
"size" : "16x16" "filename": "icon_16x16@x2.png",
"scale": "2x"
}, },
{ {
"idiom" : "mac", "size": "32x32",
"scale" : "1x", "idiom": "mac",
"size" : "32x32" "filename": "icon_32x32@x1.png",
"scale": "1x"
}, },
{ {
"idiom" : "mac", "size": "32x32",
"scale" : "2x", "idiom": "mac",
"size" : "32x32" "filename": "icon_32x32@x2.png",
"scale": "2x"
}, },
{ {
"idiom" : "mac", "size": "128x128",
"scale" : "1x", "idiom": "mac",
"size" : "128x128" "filename": "icon_128x128@x1.png",
"scale": "1x"
}, },
{ {
"idiom" : "mac", "size": "128x128",
"scale" : "2x", "idiom": "mac",
"size" : "128x128" "filename": "icon_128x128@x2.png",
"scale": "2x"
}, },
{ {
"idiom" : "mac", "size": "256x256",
"scale" : "1x", "idiom": "mac",
"size" : "256x256" "filename": "icon_256x256@x1.png",
"scale": "1x"
}, },
{ {
"idiom" : "mac", "size": "256x256",
"scale" : "2x", "idiom": "mac",
"size" : "256x256" "filename": "icon_256x256@x2.png",
"scale": "2x"
}, },
{ {
"idiom" : "mac", "size": "512x512",
"scale" : "1x", "idiom": "mac",
"size" : "512x512" "filename": "icon_512x512@x1.png",
"scale": "1x"
}, },
{ {
"idiom" : "mac", "size": "512x512",
"scale" : "2x", "idiom": "mac",
"size" : "512x512" "filename": "icon_512x512@x2.png",
"scale": "2x"
} }
], ],
"info" : { "info": {
"author" : "xcode", "author": "xcode",
"version" : 1 "version": 1
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,173 @@
#!/usr/bin/env python3
"""Generate Bugger app icon (bug beetle) in all required sizes for macOS."""
import math, os, shutil
from pathlib import Path
from PIL import Image, ImageDraw
ROOT = Path(__file__).resolve().parent.parent
ICONSET = ROOT / "Resources" / "Assets.xcassets" / "AppIcon.appiconset"
# Required macOS icon sizes: (logical_size, scale_factor, actual_pixels)
SIZES = [
(16, 1, 16), (16, 2, 32),
(32, 1, 32), (32, 2, 64),
(128, 1, 128), (128, 2, 256),
(256, 1, 256), (256, 2, 512),
(512, 1, 512), (512, 2, 1024),
]
def draw_bug(canvas_size: int) -> Image.Image:
"""Draw a bug beetle icon at given canvas size. Returns RGBA image."""
img = Image.new("RGBA", (canvas_size, canvas_size), (0, 0, 0, 0))
d = ImageDraw.Draw(img)
s = canvas_size # shorthand
# Colors
body_fill = (45, 50, 55, 255) # dark charcoal
body_highlight = (65, 72, 78, 255) # lighter shell
accent_green = (76, 188, 128, 255) # green accent
antenna = (45, 50, 55, 255)
leg = (45, 50, 55, 255)
eye = (255, 255, 255, 255)
# Margins (proportional)
margin = s * 0.12
# ---- Body (rounded oval) ----
body_rect = (
margin + s * 0.08, # left
margin + s * 0.22, # top
s - margin - s * 0.08, # right
s - margin + s * 0.05, # bottom
)
d.rounded_rectangle(body_rect, radius=s * 0.28, fill=body_fill)
# ---- Shell highlight (lighter arc on top half) ----
shell_rect = (
margin + s * 0.06,
margin + s * 0.22,
s - margin - s * 0.06,
s * 0.62,
)
d.pieslice(shell_rect, start=180, end=360, fill=body_highlight)
# ---- Green accent stripe ----
stripe_rect = (
s * 0.32,
s * 0.38,
s * 0.68,
s * 0.62,
)
d.rounded_rectangle(stripe_rect, radius=s * 0.06, fill=accent_green)
# ---- Head (small semi-circle at top) ----
head_center = (s * 0.50, s * 0.30)
head_r = s * 0.095
d.ellipse(
(head_center[0] - head_r, head_center[1] - head_r,
head_center[0] + head_r, head_center[1] + head_r),
fill=body_fill
)
# ---- Eyes ----
eye_r = s * 0.025
eye_offset = s * 0.04
for ex in (head_center[0] - eye_offset, head_center[0] + eye_offset):
d.ellipse(
(ex - eye_r, head_center[1] - eye_r - s * 0.01,
ex + eye_r, head_center[1] + eye_r - s * 0.01),
fill=eye
)
# ---- Antennae (two curved lines) ----
ant_line_w = max(2, int(s * 0.015))
ant_start_y = head_center[1] - head_r + s * 0.01
for side, curve_dir in [(-1, -0.08), (1, 0.08)]:
ant_start_x = head_center[0] + side * s * 0.04
points = [
(ant_start_x, ant_start_y),
(ant_start_x + side * s * 0.06, ant_start_y - s * 0.08),
(ant_start_x + side * s * 0.14, ant_start_y - s * 0.18 + curve_dir * s),
(ant_start_x + side * s * 0.18 + curve_dir * s * 0.5, ant_start_y - s * 0.17),
]
for i in range(len(points) - 1):
d.line([points[i], points[i + 1]], fill=antenna, width=ant_line_w)
# dot at tip
tip_r = max(1, int(s * 0.012))
tip = points[-1]
d.ellipse((tip[0] - tip_r, tip[1] - tip_r, tip[0] + tip_r, tip[1] + tip_r),
fill=antenna)
# ---- Legs (3 per side) ----
leg_w = max(2, int(s * 0.018))
leg_positions = [0.45, 0.56, 0.67] # y-positions as fraction of body height
body_left = margin + s * 0.02
body_right = s - margin - s * 0.02
for frac in leg_positions:
leg_y = s * (0.22 + frac * 0.57)
# Left legs
d.line(
[(body_left, leg_y),
(body_left - s * 0.16, leg_y - s * 0.08),
(body_left - s * 0.22, leg_y + s * 0.04)],
fill=leg, width=leg_w
)
# Right legs
d.line(
[(body_right, leg_y),
(body_right + s * 0.16, leg_y - s * 0.08),
(body_right + s * 0.22, leg_y + s * 0.04)],
fill=leg, width=leg_w
)
return img
def main():
ICONSET.mkdir(parents=True, exist_ok=True)
# Generate the largest size and downscale
master = draw_bug(1024)
images = []
for logical, scale, pixels in SIZES:
if pixels == 1024:
img = master
else:
img = master.resize((pixels, pixels), Image.LANCZOS)
filename = f"icon_{logical}x{logical}@x{scale}.png"
filepath = ICONSET / filename
img.save(filepath, "PNG")
images.append({
"size": f"{logical}x{logical}",
"scale": f"{scale}x",
"filename": filename,
})
print(f" {filename} ({pixels}×{pixels})")
# Update Contents.json
contents = {
"images": [],
"info": {"author": "xcode", "version": 1},
}
for img in images:
contents["images"].append({
"size": img["size"],
"idiom": "mac",
"filename": img["filename"],
"scale": img["scale"],
})
import json
contents_path = ICONSET / "Contents.json"
with open(contents_path, "w") as f:
json.dump(contents, f, indent=2)
print(f"\nWrote {contents_path}")
print(f"Generated {len(images)} icon sizes in {ICONSET}")
if __name__ == "__main__":
main()