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>
|
</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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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()
|
||||||