Files
SimurgArray/scripts/blog/make_featured_images.py
T
kemal 368fd30d11 branding: SimurgArray brand kit (mark, wordmark, lockups, icons, templates, guide)
Frontal dotted simurg on a bead grid: gold body and wings, three-dot crown,
red / gold / green channel plumes with peacock eyes. Detailed (31x27), simple
(11x10) and micro (7x7) drawings; dark, light, gold, black, white versions;
outlined Noto Sans wordmark; favicon and app icons; README banner, social
preview, avatar, featured-image template, report corner mark, CLI banner.
Checks: WCAG contrast table, protan/deutan simulation, icon sizes, family
sheet. Blog featured images regenerated with the new mark for consistency.
Geometry shared in scripts/brand/simurg_mark.py.
2026-09-30 07:51:52 +01:00

172 lines
7.9 KiB
Python

"""Featured images for the blog posts (1200 x 630 JPEG, WordPress / social preview size).
The SimurgArray mark (scripts/brand/simurg_mark.py) on a bead grid: gold bird, red and green channel plumes.
Run with any Python that has Pillow: python3 scripts/blog/make_featured_images.py → blog/images/*.jpg
"""
from __future__ import annotations
import math
import random
import sys
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "brand"))
from simurg_mark import SIMPLE, Grid, art, detailed
W, H, SS = 1200, 630, 2 # output size, supersampling
INDIGO, INDIGO2 = (30, 34, 64), (18, 20, 40)
RED, GREEN, GOLD, PAPER = (200, 69, 58), (47, 143, 91), (212, 160, 23), (247, 245, 239)
FONT_DIR = Path("/usr/share/fonts/truetype/noto")
OUT = Path(__file__).resolve().parents[2] / "blog" / "images"
POSTS = [
("01_why_simurgarray_should_exist", "Part 1", "Why SimurgArray\nshould exist", "box"),
("02_alternatives_and_why_i_built_it_anyway", "Part 2", "Alternatives, and\nwhy build\nit anyway", "flock"),
("03_what_good_it_could_do", "Part 3", "What good\ncould it do?", "converge"),
("04_going_public_plan_and_calendar", "Part 4", "Going public:\nthe plan and\na calendar", "calendar"),
("05_using_simurgarray_and_the_bird_behind_the_name", "Part 5", "Using SimurgArray,\nand the bird\nbehind the name",
"thirty"),
] # fmt: skip
def background(rng: random.Random) -> Image.Image:
img = Image.new("RGB", (W * SS, H * SS), INDIGO)
d = ImageDraw.Draw(img)
for y in range(H * SS): # vertical gradient
t = y / (H * SS)
c = tuple(int(a + (b - a) * t) for a, b in zip(INDIGO, INDIGO2, strict=True))
d.line([(0, y), (W * SS, y)], fill=c)
step = 18 * SS
for gy in range(step // 2, H * SS, step): # faint bead grid
for gx in range(step // 2, W * SS, step):
col = RED if rng.random() < 0.5 else GREEN
a = 0.10 + 0.08 * rng.random()
c = tuple(int(bg + (cc - bg) * a) for bg, cc in zip(INDIGO, col, strict=True))
r = 1.6 * SS
d.ellipse([gx - r, gy - r, gx + r, gy + r], fill=c)
return img
def draw_mark(img: Image.Image, grid: Grid, x: float, y: float, cell: float, dim: float = 1.0,
rad: float = 0.4) -> list[tuple[float, float]]: # fmt: skip
"""Draw a mark grid with its top left at (x, y); returns the dot centres."""
d = ImageDraw.Draw(img)
pal = {"gold": GOLD, "red": RED, "green": GREEN}
pts = []
for (c, r), layer in grid.items():
col = tuple(int(bg + (cc - bg) * dim) for bg, cc in zip(INDIGO, pal[layer], strict=True))
cx, cy = x + (c + 0.5) * cell, y + (r + 0.5) * cell
d.ellipse([cx - rad * cell, cy - rad * cell, cx + rad * cell, cy + rad * cell], fill=col)
pts.append((cx, cy))
return pts
def dot(d: ImageDraw.ImageDraw, cx: float, cy: float, r: float, col: tuple[int, int, int], a: float) -> None:
c = tuple(int(bg + (cc - bg) * a) for bg, cc in zip(INDIGO, col, strict=True))
d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=c)
def motif(img: Image.Image, kind: str, rng: random.Random) -> None:
d = ImageDraw.Draw(img)
big, _, _ = detailed()
small, sc, sr = art(SIMPLE)
if kind == "box": # a closed box, and a stream of dots leaving it to become the bird
x, y, sz = 600 * SS, 400 * SS, 140 * SS
d.rectangle([x, y, x + sz, y + sz], fill=(12, 13, 26), outline=(70, 74, 110), width=2 * SS)
cell = 14 * SS
mx, my = 730 * SS, 40 * SS
draw_mark(img, big, mx, my, cell)
x0, y0, x1, y1 = x + sz - 18 * SS, y + 18 * SS, mx + 9 * cell, my + 24 * cell
for k in range(10):
f = k / 9
col = GOLD if f > 0.6 else (RED if k % 2 else GREEN)
dot(d, x0 + (x1 - x0) * f, y0 + (y1 - y0) * f, (2.2 + 2.4 * f) * SS, col, 0.35 + 0.65 * f)
elif kind == "flock": # smaller dim birds around one bright bird
for fx, fy, cell, dim in ((575, 60, 4.2, 0.4), (600, 440, 3.6, 0.34), (1070, 470, 3.4, 0.34),
(1080, 40, 3.2, 0.3)): # fmt: skip
draw_mark(img, big, fx * SS, fy * SS, cell * SS, dim)
draw_mark(img, big, 700 * SS, 100 * SS, 13 * SS)
elif kind == "converge": # dots flowing in from the edges
pts = draw_mark(img, big, 690 * SS, 70 * SS, 15 * SS)
for _ in range(90):
tx, ty = rng.choice(pts)
ang = rng.uniform(0, 2 * math.pi)
dist = rng.uniform(120, 360) * SS
for k in range(1, 7):
f = k / 7
cx, cy = tx + math.cos(ang) * dist * f, ty + math.sin(ang) * dist * f
if cx < 620 * SS:
continue
dot(d, cx, cy, (2.6 - 1.6 * f) * SS, RED if k % 2 else GREEN, 0.5 * (1 - f))
elif kind == "calendar": # a calendar grid under the bird, some weeks done
draw_mark(img, big, 740 * SS, 26 * SS, 11 * SS)
x0, y0, cw, ch = 610 * SS, 400 * SS, 62 * SS, 34 * SS
for i in range(14):
cx, cy = x0 + (i % 7) * (cw + 8 * SS), y0 + (i // 7) * (ch + 8 * SS)
done = i < 5
d.rounded_rectangle([cx, cy, cx + cw, cy + ch], radius=6 * SS,
fill=(52, 44, 26) if done else (26, 29, 54),
outline=GOLD if done else (70, 74, 110), width=2 * SS) # fmt: skip
if done:
d.line([(cx + 20 * SS, cy + 18 * SS), (cx + 28 * SS, cy + 25 * SS), (cx + 44 * SS, cy + 10 * SS)],
fill=GOLD, width=3 * SS) # fmt: skip
elif kind == "thirty": # thirty small birds that together make the big one
cell = 15 * SS
mx, my = 680 * SS, 70 * SS
pts = draw_mark(img, big, mx, my, cell, dim=0.16)
scell = 4.2 * SS
w, h = sc * scell, sr * scell
spots: list[tuple[float, float]] = []
order = pts[:]
rng.shuffle(order)
for md in (60, 54, 48, 44, 40):
for x, y in order:
if len(spots) >= 30:
break
if all((x - a) ** 2 + (y - b) ** 2 > (md * SS) ** 2 for a, b in spots):
spots.append((x, y))
for x, y in spots[:30]:
draw_mark(img, small, x - w / 2, y - h / 2, scell)
def text(img: Image.Image, part: str, title: str) -> None:
d = ImageDraw.Draw(img)
mono = ImageFont.truetype(str(FONT_DIR / "NotoSansMono-Regular.ttf"), 26 * SS)
size = 58
while True:
bold = ImageFont.truetype(str(FONT_DIR / "NotoSans-Bold.ttf"), size * SS)
widest = max(d.textlength(line, font=bold) for line in title.split("\n"))
if widest <= 540 * SS or size <= 36:
break
size -= 2
reg = ImageFont.truetype(str(FONT_DIR / "NotoSans-Regular.ttf"), 24 * SS)
x = 70 * SS
d.text((x, 88 * SS), part.upper(), font=mono, fill=GOLD)
d.multiline_text((x, 140 * SS), title, font=bold, fill=PAPER, spacing=14 * SS)
d.ellipse([x, 540 * SS, x + 12 * SS, 552 * SS], fill=RED)
d.ellipse([x + 18 * SS, 540 * SS, x + 30 * SS, 552 * SS], fill=GREEN)
d.ellipse([x + 36 * SS, 540 * SS, x + 48 * SS, 552 * SS], fill=GOLD)
d.text((x + 66 * SS, 530 * SS), "SimurgArray", font=reg, fill=PAPER)
d.text((x + 66 * SS + d.textlength("SimurgArray ", font=reg), 530 * SS), "Thirty birds, one picture.",
font=reg, fill=(150, 154, 190)) # fmt: skip
def main() -> None:
OUT.mkdir(parents=True, exist_ok=True)
for i, (stem, part, title, kind) in enumerate(POSTS):
rng = random.Random(1000 + i)
img = background(rng)
motif(img, kind, rng)
text(img, part, title)
img = img.resize((W, H), Image.Resampling.LANCZOS)
path = OUT / f"{stem}.jpg"
img.save(path, "JPEG", quality=84, optimize=True, progressive=True)
print(f"{path.name}: {path.stat().st_size // 1024} KB")
if __name__ == "__main__":
main()