rename snaggle
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user