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.
This commit is contained in:
kemal committed 2026-09-30 07:51:52 +01:00
1 parent b2ed9682d9
commit c038d45ded
84 files changed
+823 -143

No files matched your search

+57 -143
View File
@@ -1,6 +1,6 @@
"""Featured images for the blog posts (1200 x 630 JPEG, WordPress / social preview size).
A bird drawn from dots on a BeadChip-like grid: red and green are the two scanner channels, the bird is gold.
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
"""
@@ -8,9 +8,13 @@ from __future__ import annotations
import math
import random
import sys
from pathlib import Path
from PIL import Image, ImageDraw, ImageFilter, ImageFont
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)
@@ -28,89 +32,6 @@ POSTS = [
] # fmt: skip
def _bezier(p0: tuple[float, float], p1: tuple[float, float], p2: tuple[float, float],
w0: float, w1: float, n: int = 40) -> list[tuple[float, float]]: # fmt: skip
"""Polygon of a tapered quadratic Bezier stroke (a tail plume)."""
left, right = [], []
for i in range(n + 1):
t = i / n
x = (1 - t) ** 2 * p0[0] + 2 * (1 - t) * t * p1[0] + t * t * p2[0]
y = (1 - t) ** 2 * p0[1] + 2 * (1 - t) * t * p1[1] + t * t * p2[1]
dx = 2 * (1 - t) * (p1[0] - p0[0]) + 2 * t * (p2[0] - p1[0])
dy = 2 * (1 - t) * (p1[1] - p0[1]) + 2 * t * (p2[1] - p1[1])
ln = math.hypot(dx, dy) or 1.0
hw = (w0 + (w1 - w0) * t) / 2
left.append((x - dy / ln * hw, y + dx / ln * hw))
right.append((x + dy / ln * hw, y - dx / ln * hw))
return left + right[::-1]
def bird_mask(w: int, h: int) -> Image.Image:
"""Silhouette of a simurg in flight, facing right: raised swept wings, crest, hooked beak and three long
plumes ending in peacock eyes, in a w x h box (design grid 520 x 420)."""
sx, sy = w / 520, h / 420
m = Image.new("L", (w, h), 0)
d = ImageDraw.Draw(m)
def p(pts: list[tuple[float, float]]) -> list[tuple[float, float]]:
return [(x * sx, y * sy) for x, y in pts]
def circle(cx: float, cy: float, r: float) -> None:
d.ellipse([(cx - r) * sx, (cy - r) * sy, (cx + r) * sx, (cy + r) * sy], fill=255)
body = [(250 + 95 * math.cos(t) * math.cos(-0.35) - 34 * math.sin(t) * math.sin(-0.35) + 70,
228 + 95 * math.cos(t) * math.sin(-0.35) + 34 * math.sin(t) * math.cos(-0.35))
for t in [k * math.pi / 30 for k in range(60)]] # fmt: skip
d.polygon(p(body), fill=255)
d.polygon(p([(385, 170), (420, 150), (440, 200), (395, 225)]), fill=255) # neck
circle(432, 166, 27) # head
d.polygon(p([(452, 152), (496, 166), (486, 184), (470, 178), (452, 184)]), fill=255) # hooked beak
for tip in ((372, 118), (392, 108), (414, 112)): # crest
d.polygon(p(_bezier((425, 150), ((425 + tip[0]) / 2, tip[1] + 18), tip, 12, 3)), fill=255)
d.polygon(
p(
[
(345, 205),
(338, 150),
(312, 92),
(266, 46),
(200, 12),
(222, 48),
(176, 44),
(226, 84),
(170, 92),
(236, 122),
(180, 138),
(250, 162),
(204, 186),
(284, 214),
]
),
fill=255,
) # near wing
d.polygon(
p(
[
(305, 214),
(268, 162),
(214, 124),
(140, 100),
(170, 130),
(124, 138),
(184, 162),
(146, 176),
(220, 196),
(262, 236),
]
),
fill=255,
) # far wing
for c, e, eye in (((150, 250), (22, 300), 20), ((170, 320), (58, 392), 18), ((230, 350), (170, 408), 15)):
d.polygon(p(_bezier((250, 250), c, e, 30, 8)), fill=255) # plume
circle(e[0], e[1], eye) # peacock eye
return m
def background(rng: random.Random) -> Image.Image:
img = Image.new("RGB", (W * SS, H * SS), INDIGO)
d = ImageDraw.Draw(img)
@@ -129,69 +50,59 @@ def background(rng: random.Random) -> Image.Image:
return img
def dot_bird(img: Image.Image, x0: int, y0: int, w: int, h: int, step: int, rng: random.Random,
dim: float = 1.0) -> list[tuple[float, float]]: # fmt: skip
"""Draw the bird as dots; returns the dot centres (full-resolution pixels)."""
mask = bird_mask(w, h)
edge = mask.filter(ImageFilter.MinFilter(max(3, (step // 2) | 1)))
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 yy in range(step // 2, h, step):
for xx in range(step // 2, w, step):
if mask.getpixel((xx, yy)) < 128:
continue
inner = edge.getpixel((xx, yy)) >= 128
col = GOLD if inner else (RED if (xx // step + yy // step) % 2 else GREEN)
if inner and rng.random() < 0.12:
col = (236, 196, 72)
c = tuple(int(bg + (cc - bg) * dim) for bg, cc in zip(INDIGO, col, strict=True))
r = step * (0.36 if inner else 0.30)
cx, cy = x0 + xx, y0 + yy
d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=c)
pts.append((cx, cy))
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)
bx, by, bw, bh = 640 * SS, 90 * SS, 520 * SS, 420 * SS
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 = 590 * SS, 380 * SS, 150 * SS
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)
sx0, sy0, sx1, sy1 = x + sz - 20 * SS, y + 20 * SS, 790 * SS, 300 * SS
for k in range(9):
f = k / 8
cx, cy = sx0 + (sx1 - sx0) * f, sy0 + (sy1 - sy0) * f
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)
a = 0.35 + 0.65 * f
c = tuple(int(bg + (cc - bg) * a) for bg, cc in zip(INDIGO, col, strict=True))
r = (2.2 + 2.6 * f) * SS
d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=c)
dot_bird(img, bx + 90 * SS, by - 50 * SS, int(bw * 0.82), int(bh * 0.82), 12 * SS, rng)
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, fs, dim in (
(560, 70, 0.28, 0.35),
(600, 430, 0.24, 0.3),
(1030, 470, 0.22, 0.3),
(1060, 40, 0.2, 0.28),
):
dot_bird(img, fx * SS, fy * SS, int(bw * fs), int(bh * fs), 6 * SS, rng, dim)
dot_bird(img, bx + 40 * SS, by + 20 * SS, int(bw * 0.8), int(bh * 0.8), 12 * SS, rng)
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 = dot_bird(img, bx, by, bw, bh, 13 * SS, rng)
pts = draw_mark(img, big, 690 * SS, 70 * SS, 15 * SS)
for _ in range(90):
tx, ty = rng.choice(pts)
ang = rng.uniform(math.pi * 0.55, math.pi * 1.45)
dist = rng.uniform(140, 420) * SS
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
a = 0.55 * (1 - f)
col = RED if k % 2 else GREEN
c = tuple(int(bg + (cc - bg) * a) for bg, cc in zip(INDIGO, col, strict=True))
r = (2.6 - 1.6 * f) * SS
d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=c)
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)
@@ -202,20 +113,23 @@ def motif(img: Image.Image, kind: str, rng: random.Random) -> None:
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
dot_bird(img, bx + 40 * SS, 30 * SS, int(bw * 0.72), int(bh * 0.72), 11 * SS, rng)
elif kind == "thirty": # thirty small birds that together make the big one
dot_bird(img, bx, by, bw, bh, 13 * SS, rng, dim=0.16)
mask = bird_mask(bw, bh)
spots: list[tuple[int, int]] = []
tries = 0
while len(spots) < 30 and tries < 50000:
tries += 1
x, y = rng.randrange(bw), rng.randrange(bh)
if mask.getpixel((x, y)) > 128 and all((x - a) ** 2 + (y - b) ** 2 > (50 * SS) ** 2 for a, b in spots):
spots.append((x, y))
s = 0.15
for x, y in spots:
dot_bird(img, bx + x - int(bw * s / 2), by + y - int(bh * s / 2), int(bw * s), int(bh * s), 3 * SS, rng)
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: