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>
|
|
@ -22,6 +22,13 @@
|
|||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
|
|
@ -43,4 +50,18 @@
|
|||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
|
|||
|
|
@ -1,58 +1,68 @@
|
|||
{
|
||||
"images" : [
|
||||
"images": [
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
"size": "16x16",
|
||||
"idiom": "mac",
|
||||
"filename": "icon_16x16@x1.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
"size": "16x16",
|
||||
"idiom": "mac",
|
||||
"filename": "icon_16x16@x2.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
"size": "32x32",
|
||||
"idiom": "mac",
|
||||
"filename": "icon_32x32@x1.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
"size": "32x32",
|
||||
"idiom": "mac",
|
||||
"filename": "icon_32x32@x2.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
"size": "128x128",
|
||||
"idiom": "mac",
|
||||
"filename": "icon_128x128@x1.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
"size": "128x128",
|
||||
"idiom": "mac",
|
||||
"filename": "icon_128x128@x2.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
"size": "256x256",
|
||||
"idiom": "mac",
|
||||
"filename": "icon_256x256@x1.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
"size": "256x256",
|
||||
"idiom": "mac",
|
||||
"filename": "icon_256x256@x2.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
"size": "512x512",
|
||||
"idiom": "mac",
|
||||
"filename": "icon_512x512@x1.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
"size": "512x512",
|
||||
"idiom": "mac",
|
||||
"filename": "icon_512x512@x2.png",
|
||||
"scale": "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 712 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -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()
|
||||