diff --git a/Bugger.xcodeproj/xcshareddata/xcschemes/Bugger.xcscheme b/Bugger.xcodeproj/xcshareddata/xcschemes/Bugger.xcscheme index 7d035da..ebc307b 100644 --- a/Bugger.xcodeproj/xcshareddata/xcschemes/Bugger.xcscheme +++ b/Bugger.xcodeproj/xcshareddata/xcschemes/Bugger.xcscheme @@ -22,6 +22,13 @@ + + + + + + + + diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index 3f00db4..c8bb1f2 100644 --- a/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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 } -} +} \ No newline at end of file diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@x1.png b/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@x1.png new file mode 100644 index 0000000..549305c Binary files /dev/null and b/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@x1.png differ diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@x2.png b/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@x2.png new file mode 100644 index 0000000..3386b06 Binary files /dev/null and b/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@x2.png differ diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@x1.png b/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@x1.png new file mode 100644 index 0000000..03019d2 Binary files /dev/null and b/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@x1.png differ diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@x2.png b/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@x2.png new file mode 100644 index 0000000..724af4f Binary files /dev/null and b/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@x2.png differ diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@x1.png b/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@x1.png new file mode 100644 index 0000000..3386b06 Binary files /dev/null and b/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@x1.png differ diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@x2.png b/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@x2.png new file mode 100644 index 0000000..235334e Binary files /dev/null and b/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@x2.png differ diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@x1.png b/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@x1.png new file mode 100644 index 0000000..724af4f Binary files /dev/null and b/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@x1.png differ diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@x2.png b/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@x2.png new file mode 100644 index 0000000..ca61186 Binary files /dev/null and b/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@x2.png differ diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@x1.png b/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@x1.png new file mode 100644 index 0000000..235334e Binary files /dev/null and b/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@x1.png differ diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@x2.png b/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@x2.png new file mode 100644 index 0000000..c08879f Binary files /dev/null and b/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@x2.png differ diff --git a/scripts/generate_app_icon.py b/scripts/generate_app_icon.py new file mode 100644 index 0000000..1cf1b75 --- /dev/null +++ b/scripts/generate_app_icon.py @@ -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()