Files
2026-09-18 07:17:34 -05:00

123 lines
4.7 KiB
Python

#!/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()