Files
sandcage/scripts/make_icon.py
T
2026-05-23 15:29:45 +02:00

104 lines
3.7 KiB
Python

"""Generate icon variants from the sandcage source image."""
from pathlib import Path
from PIL import Image, ImageDraw
ROOT = Path(__file__).resolve().parent.parent
SRC = ROOT / ".workpad" / "assets" / "sandcage.png"
OUT = ROOT / "local"
def square_crop(src: Path, size: int = 1024) -> Image.Image:
img = Image.open(src).convert("RGBA")
w, h = img.size
side = min(w, h)
left = (w - side) // 2
top = (h - side) // 2
img = img.crop((left, top, left + side, top + side))
return img.resize((size, size), Image.LANCZOS)
def invert_rgb(img: Image.Image) -> Image.Image:
from PIL import ImageChops
r, g, b, a = img.split()
return Image.merge("RGBA", (ImageChops.invert(r), ImageChops.invert(g), ImageChops.invert(b), a))
def lighten_dune_interior(img: Image.Image, grey: int = 80) -> Image.Image:
img = img.copy()
draw = ImageDraw.Draw(img)
fill = (grey, grey, grey, 255)
# tips found at (56,567) and (973,567), dune bottom at y=652
# draw a filled arc (pieslice) from tip to tip curving below to seal the interior
# pieslice bbox: center the ellipse so it passes through both tips
cx = (56 + 973) // 2
half_w = (973 - 56) // 2
arc_depth = 120
bbox = (cx - half_w, 567 - arc_depth, cx + half_w, 567 + arc_depth)
# draw just the bottom arc in grey to create the barrier
draw.arc(bbox, 0, 180, fill=fill, width=4)
# flood fill the enclosed black region
ImageDraw.floodfill(img, (300, 620), fill, thresh=60)
return img
def apply_vertical_bars(img: Image.Image, bar_width: int = 48, gap_width: int = 48, opacity: float = 0.5) -> Image.Image:
img = img.copy()
r, g, b, a = img.split()
bar_mask = Image.new("L", img.size, 255)
draw = ImageDraw.Draw(bar_mask)
bar_alpha = int(255 * opacity)
x = 0
while x < img.size[0]:
draw.rectangle((x, 0, x + bar_width - 1, img.size[1]), fill=bar_alpha)
x += bar_width + gap_width
from PIL import ImageChops
a = ImageChops.multiply(a, bar_mask)
return Image.merge("RGBA", (r, g, b, a))
def apply_circle_mask(img: Image.Image, size: int = 1024) -> Image.Image:
# circle passes through dune tips (56,567) and (973,567)
# center at midpoint, radius = half the tip distance
cx = (56 + 973) // 2 # 514
cy = 567
r = (973 - 56) // 2 # 458
# crop to bounding box of this circle, then resize to output size
left = max(cx - r, 0)
top = max(cy - r, 0)
right = min(cx + r, img.size[0])
bottom = min(cy + r, img.size[1])
cropped = img.crop((left, top, right, bottom))
cropped = cropped.resize((size, size), Image.LANCZOS)
# apply circular mask
mask = Image.new("L", (size, size), 0)
draw = ImageDraw.Draw(mask)
draw.ellipse((0, 0, size, size), fill=255)
result = Image.new("RGBA", (size, size), (0, 0, 0, 0))
result.paste(cropped, (0, 0), mask)
return result
def save(img: Image.Image, path: Path) -> Path:
img.save(path)
print(f"Wrote {path}")
return path
if __name__ == "__main__":
OUT.mkdir(parents=True, exist_ok=True)
base = square_crop(SRC)
inverted = invert_rgb(base)
save(apply_circle_mask(base), OUT / "sandcage-round.png")
save(apply_circle_mask(inverted), OUT / "sandcage-round-inverted.png")
inverted_grey = lighten_dune_interior(inverted)
save(apply_circle_mask(inverted_grey), OUT / "sandcage-round-inverted-grey.png")
save(apply_circle_mask(apply_vertical_bars(base)), OUT / "sandcage-round-bars.png")
save(apply_circle_mask(apply_vertical_bars(inverted)), OUT / "sandcage-round-inverted-bars.png")
save(apply_circle_mask(apply_vertical_bars(inverted_grey)), OUT / "sandcage-round-inverted-grey-bars.png")