104 lines
3.7 KiB
Python
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")
|