#!/usr/bin/env python3 """Generates Snaggle's app icon set. Draws an original mark - a rounded square with a capture viewfinder - at high resolution, then writes every size macOS asks for into the asset catalogue. Run it again after editing the colours or shapes below: python3 scripts/make_appicon.py """ from pathlib import Path from PIL import Image, ImageDraw, ImageFilter SUPERSAMPLE = 4 BASE = 1024 OUTPUT = Path(__file__).resolve().parent.parent / "Snaggle" / "Resources" / "Assets.xcassets" / "AppIcon.appiconset" # Deep to bright green, reading as one surface rather than a flat fill. TOP_COLOR = (46, 173, 92) BOTTOM_COLOR = (18, 106, 62) MARK_COLOR = (255, 255, 255, 242) def vertical_gradient(size, top, bottom): gradient = Image.new("RGB", (1, size), top) for y in range(size): blend = y / max(1, size - 1) gradient.putpixel((0, y), tuple( round(top[channel] + (bottom[channel] - top[channel]) * blend) for channel in range(3) )) return gradient.resize((size, size)) def draw_icon(size): scale = size / BASE canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0)) # macOS leaves a margin around the rounded square and uses a large corner radius. margin = round(100 * scale) side = size - margin * 2 radius = round(185 * scale) mask = Image.new("L", (size, size), 0) ImageDraw.Draw(mask).rounded_rectangle( [margin, margin, size - margin, size - margin], radius=radius, fill=255) body = Image.new("RGBA", (size, size), (0, 0, 0, 0)) body.paste(vertical_gradient(size, TOP_COLOR, BOTTOM_COLOR), (0, 0)) body.putalpha(mask) # Soft highlight across the top, blurred so there is no hard glossy edge. highlight = Image.new("RGBA", (size, size), (0, 0, 0, 0)) ImageDraw.Draw(highlight).ellipse( [margin - side * 0.35, margin - side * 0.95, size - margin + side * 0.35, margin + side * 0.40], fill=(255, 255, 255, 34)) highlight = highlight.filter(ImageFilter.GaussianBlur(round(60 * scale))) highlight.putalpha(Image.composite(highlight.split()[3], Image.new("L", (size, size), 0), mask)) body = Image.alpha_composite(body, highlight) canvas = Image.alpha_composite(canvas, body) # The mark: four viewfinder corners around an aperture ring. draw = ImageDraw.Draw(canvas) stroke = max(1, round(46 * scale)) centre = size / 2 reach = side * 0.30 # how far the corners sit from the centre arm = side * 0.13 # length of each corner arm def round_cap(x, y): draw.ellipse([x - stroke / 2, y - stroke / 2, x + stroke / 2, y + stroke / 2], fill=MARK_COLOR) for x_sign in (-1, 1): for y_sign in (-1, 1): x = centre + x_sign * reach y = centre + y_sign * reach # One polyline per corner keeps the elbow smooth instead of notched. start = (x - x_sign * arm, y) elbow = (x, y) end = (x, y - y_sign * arm) draw.line([start, elbow, end], fill=MARK_COLOR, width=stroke, joint="curve") for point in (start, elbow, end): round_cap(*point) ring = side * 0.135 draw.ellipse([centre - ring, centre - ring, centre + ring, centre + ring], outline=MARK_COLOR, width=stroke) # Shadow under the rounded square, the way system icons sit on a surface. shadow = Image.new("RGBA", (size, size), (0, 0, 0, 0)) ImageDraw.Draw(shadow).rounded_rectangle( [margin, margin + round(18 * scale), size - margin, size - margin + round(18 * scale)], radius=radius, fill=(0, 0, 0, 70)) shadow = shadow.filter(ImageFilter.GaussianBlur(round(18 * scale))) return Image.alpha_composite(shadow, canvas) def main(): OUTPUT.mkdir(parents=True, exist_ok=True) master = draw_icon(BASE * SUPERSAMPLE).resize((BASE, BASE), Image.LANCZOS) # (point size, scale) pairs macOS expects for a Mac app icon. entries = [] for point, scales in [(16, [1, 2]), (32, [1, 2]), (128, [1, 2]), (256, [1, 2]), (512, [1, 2])]: for factor in scales: pixels = point * factor name = f"icon_{point}x{point}{'@2x' if factor == 2 else ''}.png" master.resize((pixels, pixels), Image.LANCZOS).save(OUTPUT / name) entries.append(f""" {{ "filename" : "{name}", "idiom" : "mac", "scale" : "{factor}x", "size" : "{point}x{point}" }}""") contents = "{\n \"images\" : [\n" + ",\n".join(entries) + \ "\n ],\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n" (OUTPUT / "Contents.json").write_text(contents) print(f"Wrote {len(entries)} icon sizes to {OUTPUT}") if __name__ == "__main__": main()