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:
1 parent
ba38be122e
commit
558c198957
84 files changed
+823
-143
No files matched your search
@@ -0,0 +1,452 @@
|
||||
"""SimurgArray brand kit: marks, wordmark, lockups, icons, templates and checks → branding/.
|
||||
|
||||
Needs Pillow and fontTools (for the wordmark outlines), rsvg-convert and ImageMagick:
|
||||
python -m venv /tmp/b && /tmp/b/bin/pip install pillow fonttools && /tmp/b/bin/python scripts/brand/make_brand.py
|
||||
|
||||
The mark is drawn on a dot grid in grid units (capsules and circles tested at each cell centre), so every export is
|
||||
the same set of dots: a frontal simurg with raised wings, a three-dot crown and three tail plumes (red channel,
|
||||
gold, green channel) ending in peacock eyes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from fontTools.pens.svgPathPen import SVGPathPen
|
||||
from fontTools.pens.transformPen import TransformPen
|
||||
from fontTools.ttLib import TTFont
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
OUT = ROOT / "branding"
|
||||
FONTS = Path("/usr/share/fonts/truetype/noto")
|
||||
|
||||
C = {
|
||||
"indigo": "#1E2240", "indigo_deep": "#12142A", "paper": "#F7F5EF", "red": "#C8453A", "green": "#2F8F5B",
|
||||
"gold": "#D4A017", "gold_deep": "#A87A00", "gold_ink": "#8C6500", "muted_dark": "#A3A7C9",
|
||||
"muted_light": "#5A5E7E", "black": "#111111", "white": "#FFFFFF",
|
||||
} # fmt: skip
|
||||
CHART = {"AA": ("#D55E00", "circle"), "AB": ("#009E73", "triangle"), "BB": ("#0072B2", "square")} # Okabe and Ito
|
||||
SCHEMES = { # layer colours per version
|
||||
"dark": {"gold": C["gold"], "red": C["red"], "green": C["green"], "bg": C["indigo"], "text": C["paper"]},
|
||||
"light": {"gold": C["gold_deep"], "red": C["red"], "green": C["green"], "bg": C["paper"], "text": C["indigo"]},
|
||||
"gold": {"gold": C["gold"], "red": C["gold"], "green": C["gold"], "bg": None, "text": C["gold"]},
|
||||
"black": {"gold": C["black"], "red": C["black"], "green": C["black"], "bg": None, "text": C["black"]},
|
||||
"white": {"gold": C["white"], "red": C["white"], "green": C["white"], "bg": None, "text": C["white"]},
|
||||
}
|
||||
ACCENT = {"dark": C["gold"], "light": C["gold_ink"], "gold": C["gold"], "black": C["black"], "white": C["white"]}
|
||||
|
||||
# ---------------------------------------------------------------- the mark (geometry in simurg_mark.py)
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from simurg_mark import MICRO, SIMPLE, Grid, art, detailed # noqa: E402
|
||||
|
||||
MARKS = {"mark": detailed(), "mark_simple": art(SIMPLE), "mark_micro": art(MICRO)}
|
||||
|
||||
|
||||
def mark_svg_body(grid: Grid, scheme: str, x0: float = 0, y0: float = 0, cell: float = 1.0, rad: float = 0.4) -> str:
|
||||
col = SCHEMES[scheme]
|
||||
out = []
|
||||
for layer in ("gold", "red", "green"):
|
||||
cells = sorted((rc for rc, lay in grid.items() if lay == layer), key=lambda k: (k[1], k[0]))
|
||||
dots = [f'<circle cx="{x0 + (c + 0.5) * cell:.3f}" cy="{y0 + (r + 0.5) * cell:.3f}" r="{rad * cell:.3f}"/>'
|
||||
for c, r in cells] # fmt: skip
|
||||
if dots:
|
||||
out.append(f'<g fill="{col[layer]}">' + "".join(dots) + "</g>")
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def svg(w: float, h: float, body: str, bg: str | None = None, title: str = "SimurgArray") -> str:
|
||||
rect = f'<rect width="{w}" height="{h}" fill="{bg}"/>' if bg else ""
|
||||
return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w:g} {h:g}" width="{w:g}" height="{h:g}" '
|
||||
f'role="img" aria-label="{title}"><title>{title}</title>{rect}{body}</svg>\n') # fmt: skip
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- the wordmark (outlined Noto Sans SemiBold)
|
||||
|
||||
|
||||
class Wordmark:
|
||||
def __init__(self, path: Path) -> None:
|
||||
self.font = TTFont(path)
|
||||
self.gs = self.font.getGlyphSet()
|
||||
self.cmap = self.font.getBestCmap()
|
||||
self.upm = self.font["head"].unitsPerEm
|
||||
self.hmtx = self.font["hmtx"]
|
||||
|
||||
def paths(self, text: str, size: float, x: float, baseline: float, tracking: float = 0.0) -> tuple[str, float]:
|
||||
"""SVG path data for ``text`` at ``size`` px, starting at x; returns (d, advance)."""
|
||||
s = size / self.upm
|
||||
ds, pen_x = [], x
|
||||
for ch in text:
|
||||
gname = self.cmap[ord(ch)]
|
||||
pen = SVGPathPen(self.gs)
|
||||
self.gs[gname].draw(TransformPen(pen, (s, 0, 0, -s, pen_x, baseline)))
|
||||
ds.append(pen.getCommands())
|
||||
pen_x += self.hmtx[gname][0] * s + tracking * size
|
||||
return " ".join(ds), pen_x - x - tracking * size
|
||||
|
||||
def cap_height(self, size: float) -> float:
|
||||
return float(getattr(self.font["OS/2"], "sCapHeight", 714)) * size / self.upm
|
||||
|
||||
|
||||
WM = Wordmark(FONTS / "NotoSans-SemiBold.ttf")
|
||||
|
||||
|
||||
def wordmark_body(scheme: str, x: float, baseline: float, size: float) -> tuple[str, float]:
|
||||
d1, w1 = WM.paths("Simurg", size, x, baseline, tracking=0.005)
|
||||
d2, w2 = WM.paths("Array", size, x + w1 + size * 0.03, baseline, tracking=0.005)
|
||||
body = f'<path fill="{SCHEMES[scheme]["text"]}" d="{d1}"/><path fill="{ACCENT[scheme]}" d="{d2}"/>'
|
||||
return body, w1 + size * 0.03 + w2
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- files
|
||||
|
||||
|
||||
def write(path: Path, text: str) -> Path:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text)
|
||||
return path
|
||||
|
||||
|
||||
def png(svg_path: Path, out: Path, width: int) -> None:
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
subprocess.run(["rsvg-convert", "-w", str(width), "-o", str(out), str(svg_path)], check=True)
|
||||
|
||||
|
||||
def logos() -> None:
|
||||
for name, (g, cols, rows) in MARKS.items():
|
||||
pad = 1
|
||||
for scheme in SCHEMES:
|
||||
p = write(OUT / "logo" / f"{name}_{scheme}.svg",
|
||||
svg(cols + 2 * pad, rows + 2 * pad, mark_svg_body(g, scheme, pad, pad))) # fmt: skip
|
||||
png(p, OUT / "logo" / "png" / f"{name}_{scheme}.png", 512 if name == "mark" else 256)
|
||||
size = 100.0
|
||||
for scheme in SCHEMES:
|
||||
body, w = wordmark_body(scheme, 0, size * 0.78, size)
|
||||
p = write(OUT / "logo" / f"wordmark_{scheme}.svg", svg(w + 4, size, body))
|
||||
png(p, OUT / "logo" / "png" / f"wordmark_{scheme}.png", 900)
|
||||
g, cols, rows = MARKS["mark"]
|
||||
for scheme in ("dark", "light", "black", "white"):
|
||||
bg = SCHEMES[scheme]["bg"]
|
||||
# horizontal: mark 120 px high, wordmark cap height ~ 40 % of it
|
||||
cell = 120 / rows
|
||||
ms = mark_svg_body(g, scheme, 40, 40, cell)
|
||||
wsize = 64.0
|
||||
wb, ww = wordmark_body(scheme, 40 + cols * cell + 36, 40 + 60 + WM.cap_height(wsize) / 2, wsize)
|
||||
W, H = 40 + cols * cell + 36 + ww + 48, 200
|
||||
p = write(OUT / "logo" / f"lockup_horizontal_{scheme}.svg", svg(W, H, ms + wb, bg))
|
||||
png(p, OUT / "logo" / "png" / f"lockup_horizontal_{scheme}.png", 1200)
|
||||
# stacked
|
||||
cell = 200 / rows
|
||||
wsize = 60.0
|
||||
_, ww = wordmark_body(scheme, 0, 0, wsize)
|
||||
W = max(cols * cell, ww) + 80
|
||||
ms = mark_svg_body(g, scheme, (W - cols * cell) / 2, 40, cell)
|
||||
wb, _ = wordmark_body(scheme, (W - ww) / 2, 40 + rows * cell + 30 + WM.cap_height(wsize), wsize)
|
||||
p = write(OUT / "logo" / f"lockup_stacked_{scheme}.svg", svg(W, 40 + rows * cell + 30 + 90, ms + wb, bg))
|
||||
png(p, OUT / "logo" / "png" / f"lockup_stacked_{scheme}.png", 800)
|
||||
|
||||
|
||||
def icons() -> None:
|
||||
"""Rounded indigo tile; micro mark at 16, simple at 32/48, detailed from 180 up."""
|
||||
d = OUT / "icons"
|
||||
for size, name in ((16, "mark_micro"), (32, "mark_simple"), (48, "mark_simple"), (180, "mark"), (512, "mark")):
|
||||
g, cols, rows = MARKS[name]
|
||||
n = max(cols, rows) + (1 if size <= 48 else 3)
|
||||
rx = n * 0.18
|
||||
body = f'<rect width="{n}" height="{n}" rx="{rx:.2f}" fill="{C["indigo"]}"/>'
|
||||
body += mark_svg_body(g, "dark", (n - cols) / 2, (n - rows) / 2, 1.0, 0.42 if size <= 48 else 0.4)
|
||||
p = write(d / f"icon_{size}.svg", svg(n, n, body))
|
||||
png(p, d / f"icon_{size}.png", size)
|
||||
subprocess.run(["magick", str(d / "icon_16.png"), str(d / "icon_32.png"), str(d / "icon_48.png"),
|
||||
str(d / "favicon.ico")], check=True) # fmt: skip
|
||||
for f in d.glob("icon_*.svg"):
|
||||
if f.stem not in ("icon_16", "icon_512"):
|
||||
f.unlink()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- raster templates (Pillow)
|
||||
|
||||
|
||||
def rgb(h: str) -> tuple[int, int, int]:
|
||||
return int(h[1:3], 16), int(h[3:5], 16), int(h[5:7], 16)
|
||||
|
||||
|
||||
def canvas(w: int, h: int, ss: int = 2) -> Image.Image:
|
||||
img = Image.new("RGB", (w * ss, h * ss), rgb(C["indigo"]))
|
||||
dr = ImageDraw.Draw(img)
|
||||
a, b = rgb(C["indigo"]), rgb(C["indigo_deep"])
|
||||
for y in range(h * ss):
|
||||
t = y / (h * ss)
|
||||
dr.line([(0, y), (w * ss, y)], fill=tuple(int(x + (z - x) * t) for x, z in zip(a, b, strict=True)))
|
||||
step, k = 18 * ss, 0
|
||||
for gy in range(step // 2, h * ss, step): # faint bead grid, alternating channels
|
||||
for gx in range(step // 2, w * ss, step):
|
||||
k += 1
|
||||
col = rgb(C["red"] if (gx // step + gy // step) % 2 else C["green"])
|
||||
c = tuple(int(bg + (cc - bg) * 0.14) for bg, cc in zip(a, col, strict=True))
|
||||
r = 1.6 * ss
|
||||
dr.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, scheme: str = "dark",
|
||||
rad: float = 0.4) -> None: # fmt: skip
|
||||
dr = ImageDraw.Draw(img)
|
||||
col = SCHEMES[scheme]
|
||||
for (c, r), layer in grid.items():
|
||||
cx, cy = x + (c + 0.5) * cell, y + (r + 0.5) * cell
|
||||
dr.ellipse([cx - rad * cell, cy - rad * cell, cx + rad * cell, cy + rad * cell], fill=rgb(col[layer]))
|
||||
|
||||
|
||||
def font(name: str, px: float) -> ImageFont.FreeTypeFont:
|
||||
return ImageFont.truetype(str(FONTS / name), int(px))
|
||||
|
||||
|
||||
def text_wordmark(img: Image.Image, x: float, y: float, px: float) -> float:
|
||||
dr = ImageDraw.Draw(img)
|
||||
f = font("NotoSans-SemiBold.ttf", px)
|
||||
dr.text((x, y), "Simurg", font=f, fill=rgb(C["paper"]))
|
||||
w = dr.textlength("Simurg", font=f) + px * 0.03
|
||||
dr.text((x + w, y), "Array", font=f, fill=rgb(C["gold"]))
|
||||
return w + dr.textlength("Array", font=f)
|
||||
|
||||
|
||||
def save(img: Image.Image, w: int, h: int, path: Path, quality: int = 86) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
img = img.resize((w, h), Image.Resampling.LANCZOS)
|
||||
if path.suffix == ".jpg":
|
||||
img.save(path, "JPEG", quality=quality, optimize=True, progressive=True)
|
||||
else:
|
||||
img.save(path, "PNG", optimize=True)
|
||||
|
||||
|
||||
def templates() -> None:
|
||||
ss = 2
|
||||
g, cols, rows = MARKS["mark"]
|
||||
t = OUT / "templates"
|
||||
# README banner 1280 x 320
|
||||
img = canvas(1280, 320, ss)
|
||||
cell = 230 * ss / rows
|
||||
draw_mark(img, g, 70 * ss, 45 * ss, cell)
|
||||
x = 70 * ss + cols * cell + 50 * ss
|
||||
text_wordmark(img, x, 88 * ss, 76 * ss)
|
||||
ImageDraw.Draw(img).text((x + 4 * ss, 196 * ss), "Thirty birds, one picture.", font=font("NotoSans-Regular.ttf",
|
||||
30 * ss), fill=rgb(C["muted_dark"])) # fmt: skip
|
||||
save(img, 1280, 320, t / "readme_banner_1280x320.png")
|
||||
# repository social preview 1280 x 640
|
||||
img = canvas(1280, 640, ss)
|
||||
cell = 380 * ss / rows
|
||||
draw_mark(img, g, 90 * ss, 110 * ss, cell)
|
||||
x = 90 * ss + cols * cell + 60 * ss
|
||||
text_wordmark(img, x, 200 * ss, 84 * ss)
|
||||
dr = ImageDraw.Draw(img)
|
||||
dr.text((x + 4 * ss, 320 * ss), "Thirty birds, one picture.", font=font("NotoSans-Regular.ttf", 34 * ss),
|
||||
fill=rgb(C["muted_dark"])) # fmt: skip
|
||||
dr.text((x + 4 * ss, 400 * ss), "Open Infinium array analysis:\nIDAT to genotypes, QC, CN and PGx",
|
||||
font=font("NotoSansMono-Regular.ttf", 24 * ss), fill=rgb(C["gold"]), spacing=10 * ss) # fmt: skip
|
||||
save(img, 1280, 640, t / "social_preview_1280x640.png")
|
||||
# avatar 512, safe inside a circle (mark within 70 % of the diameter)
|
||||
img = Image.new("RGB", (512 * ss, 512 * ss), rgb(C["indigo"]))
|
||||
cell = 512 * ss * 0.62 / max(cols, rows)
|
||||
draw_mark(img, g, (512 * ss - cols * cell) / 2, (512 * ss - rows * cell) / 2 + 6 * ss, cell)
|
||||
save(img, 512, 512, t / "avatar_512.png")
|
||||
# WordPress featured image template: title area left, mark right
|
||||
img = canvas(1200, 630, ss)
|
||||
cell = 400 * ss / rows
|
||||
draw_mark(img, g, 1200 * ss - cols * cell - 70 * ss, 100 * ss, cell)
|
||||
dr = ImageDraw.Draw(img)
|
||||
dr.text((70 * ss, 88 * ss), "PART N", font=font("NotoSansMono-Regular.ttf", 26 * ss), fill=rgb(C["gold"]))
|
||||
dr.multiline_text((70 * ss, 140 * ss), "Post title in\ntwo or three lines", font=font("NotoSans-Bold.ttf", 58 * ss),
|
||||
fill=rgb(C["paper"]), spacing=14 * ss) # fmt: skip
|
||||
footer(img, 70 * ss, 530 * ss, ss)
|
||||
save(img, 1200, 630, t / "featured_template_1200x630.jpg")
|
||||
|
||||
|
||||
def footer(img: Image.Image, x: float, y: float, ss: int) -> None:
|
||||
dr = ImageDraw.Draw(img)
|
||||
for i, k in enumerate(("red", "green", "gold")):
|
||||
dr.ellipse([x + i * 18 * ss, y + 10 * ss, x + i * 18 * ss + 12 * ss, y + 22 * ss], fill=rgb(C[k]))
|
||||
f = font("NotoSans-Regular.ttf", 24 * ss)
|
||||
dr.text((x + 66 * ss, y), "SimurgArray", font=f, fill=rgb(C["paper"]))
|
||||
dr.text((x + 66 * ss + dr.textlength("SimurgArray ", font=f), y), "Thirty birds, one picture.", font=f,
|
||||
fill=rgb(C["muted_dark"])) # fmt: skip
|
||||
|
||||
|
||||
def report_mark() -> None:
|
||||
g, cols, rows = MARKS["mark_simple"]
|
||||
body = mark_svg_body(g, "dark", 1, 1)
|
||||
write(OUT / "templates" / "report_corner_mark.svg", svg(cols + 2, rows + 2, body))
|
||||
style = f"display:flex;align-items:center;gap:8px;font:12px 'Noto Sans Mono',monospace;color:{C['muted_dark']}"
|
||||
write(OUT / "templates" / "report_corner_mark.html", f"""<!-- SimurgArray report corner mark.
|
||||
Always shown with the software version and the run manifest checksum. -->
|
||||
<div class="simurg-mark" style="{style}">
|
||||
<img src="report_corner_mark.svg" width="26" height="24" alt="SimurgArray">
|
||||
<span>SimurgArray {{version}} · run {{manifest_sha256[:12]}}</span>
|
||||
</div>
|
||||
""") # fmt: skip
|
||||
|
||||
|
||||
CLI_BANNER = r"""
|
||||
o o SimurgArray {version}
|
||||
oo o oo Thirty birds, one picture.
|
||||
ooooooo
|
||||
ooo IDAT to genotypes, QC, CN, PGx
|
||||
/ | \ research use only
|
||||
* o *
|
||||
"""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- checks
|
||||
|
||||
|
||||
def _lin(c: float) -> float:
|
||||
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
|
||||
|
||||
|
||||
def _unlin(c: float) -> int:
|
||||
c = max(0.0, min(1.0, c))
|
||||
return round(255 * (12.92 * c if c <= 0.0031308 else 1.055 * c ** (1 / 2.4) - 0.055))
|
||||
|
||||
|
||||
def contrast(a: str, b: str) -> float:
|
||||
def lum(h: str) -> float:
|
||||
r, g, bb = (_lin(v / 255) for v in rgb(h))
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * bb
|
||||
|
||||
la, lb = sorted((lum(a), lum(b)), reverse=True)
|
||||
return (la + 0.05) / (lb + 0.05)
|
||||
|
||||
|
||||
MACHADO = { # Machado, Oliveira and Fernandes 2009, severity 1.0 (linear RGB)
|
||||
"protanopia": ((0.152286, 1.052583, -0.204868), (0.114503, 0.786281, 0.099216), (-0.003882, -0.048116, 1.051998)),
|
||||
"deuteranopia": ((0.367322, 0.860646, -0.227968), (0.280085, 0.672501, 0.047413),
|
||||
(-0.011820, 0.042940, 0.968881)),
|
||||
} # fmt: skip
|
||||
|
||||
|
||||
def simulate(img: Image.Image, kind: str) -> Image.Image:
|
||||
m = MACHADO[kind]
|
||||
lut = [_lin(i / 255) for i in range(256)]
|
||||
px = img.convert("RGB").load()
|
||||
out = img.convert("RGB").copy()
|
||||
po = out.load()
|
||||
cache: dict[tuple[int, int, int], tuple[int, int, int]] = {}
|
||||
for y in range(img.height):
|
||||
for x in range(img.width):
|
||||
v = px[x, y]
|
||||
if v not in cache:
|
||||
r, g, b = (lut[c] for c in v)
|
||||
cache[v] = tuple(_unlin(row[0] * r + row[1] * g + row[2] * b) for row in m) # type: ignore[assignment]
|
||||
po[x, y] = cache[v]
|
||||
return out
|
||||
|
||||
|
||||
def chart_example(ss: int = 2) -> Image.Image:
|
||||
"""A genotype cluster plot in the chart palette: colour plus marker shape."""
|
||||
import random
|
||||
|
||||
rng = random.Random(7)
|
||||
w, h = 420, 300
|
||||
img = Image.new("RGB", (w * ss, h * ss), rgb(C["paper"]))
|
||||
dr = ImageDraw.Draw(img)
|
||||
dr.line([(40 * ss, 260 * ss), (400 * ss, 260 * ss)], fill=rgb(C["muted_light"]), width=ss)
|
||||
dr.line([(40 * ss, 20 * ss), (40 * ss, 260 * ss)], fill=rgb(C["muted_light"]), width=ss)
|
||||
for gt, cx in (("AA", 90), ("AB", 220), ("BB", 350)):
|
||||
col, shape = CHART[gt]
|
||||
for _ in range(40):
|
||||
x, y = (cx + rng.gauss(0, 14)) * ss, (140 + rng.gauss(0, 30)) * ss
|
||||
r = 5 * ss
|
||||
if shape == "circle":
|
||||
dr.ellipse([x - r, y - r, x + r, y + r], fill=rgb(col))
|
||||
elif shape == "square":
|
||||
dr.rectangle([x - r * 0.9, y - r * 0.9, x + r * 0.9, y + r * 0.9], fill=rgb(col))
|
||||
else:
|
||||
dr.polygon([(x, y - r * 1.1), (x - r, y + r * 0.8), (x + r, y + r * 0.8)], fill=rgb(col))
|
||||
dr.text((cx * ss - 12 * ss, 268 * ss), gt, font=font("NotoSansMono-Regular.ttf", 16 * ss),
|
||||
fill=rgb(C["indigo"])) # fmt: skip
|
||||
return img.resize((w, h), Image.Resampling.LANCZOS)
|
||||
|
||||
|
||||
def checks() -> list[tuple[str, str, str, float]]:
|
||||
d = OUT / "checks"
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
g, cols, rows = MARKS["mark"]
|
||||
logo = Image.new("RGB", (cols * 12 + 24, rows * 12 + 24), rgb(C["indigo"]))
|
||||
draw_mark(logo, g, 12, 12, 12)
|
||||
chart = chart_example()
|
||||
panels = [("original", logo, chart)] + [(k, simulate(logo, k), simulate(chart, k)) for k in MACHADO]
|
||||
pw = logo.width + chart.width + 30
|
||||
ph = max(logo.height, chart.height) + 50
|
||||
sheet = Image.new("RGB", (pw, ph * 3), rgb(C["paper"]))
|
||||
dr = ImageDraw.Draw(sheet)
|
||||
for i, (name, lg, ch) in enumerate(panels):
|
||||
y = i * ph
|
||||
dr.text((10, y + 8), name, font=font("NotoSansMono-Regular.ttf", 18), fill=rgb(C["indigo"]))
|
||||
sheet.paste(lg, (10, y + 36))
|
||||
sheet.paste(ch, (logo.width + 20, y + 36))
|
||||
sheet.save(d / "colour_vision_check.png", optimize=True)
|
||||
# small sizes side by side
|
||||
row = Image.new("RGB", (520, 90), rgb(C["paper"]))
|
||||
x = 10
|
||||
for s in (16, 32, 48, 180):
|
||||
ic = Image.open(OUT / "icons" / f"icon_{s}.png").convert("RGB")
|
||||
if s == 180:
|
||||
ic = ic.resize((72, 72), Image.Resampling.LANCZOS)
|
||||
row.paste(ic, (x, 10))
|
||||
x += ic.width + 30
|
||||
row.save(d / "icon_sizes.png", optimize=True)
|
||||
pairs = [("paper", "indigo", "text on dark"), ("muted_dark", "indigo", "secondary text on dark"),
|
||||
("gold", "indigo", "accent text on dark"), ("indigo", "paper", "text on light"),
|
||||
("muted_light", "paper", "secondary text on light"), ("gold_ink", "paper", "accent text on light"),
|
||||
("red", "indigo", "graphics on dark"), ("green", "indigo", "graphics on dark"),
|
||||
("gold_deep", "paper", "graphics on light"), ("red", "paper", "graphics on light"),
|
||||
("green", "paper", "graphics on light"), ("gold", "paper", "never for text or small marks")] # fmt: skip
|
||||
return [(a, b, use, contrast(C[a], C[b])) for a, b, use in pairs]
|
||||
|
||||
|
||||
def family(featured: list[Path]) -> None:
|
||||
"""Featured images and templates side by side (acceptance check 4)."""
|
||||
tiles = [Image.open(p).convert("RGB") for p in featured] + [
|
||||
Image.open(OUT / "templates" / "featured_template_1200x630.jpg").convert("RGB"),
|
||||
Image.open(OUT / "templates" / "social_preview_1280x640.png").convert("RGB"),
|
||||
Image.open(OUT / "templates" / "readme_banner_1280x320.png").convert("RGB"),
|
||||
Image.open(OUT / "templates" / "avatar_512.png").convert("RGB"),
|
||||
] # fmt: skip
|
||||
th = 200
|
||||
tiles = [t.resize((int(t.width * th / t.height), th), Image.Resampling.LANCZOS) for t in tiles]
|
||||
per_row, gap = 3, 16
|
||||
rows_ = [tiles[i : i + per_row] for i in range(0, len(tiles), per_row)]
|
||||
W = max(sum(t.width for t in r) + gap * (len(r) + 1) for r in rows_)
|
||||
sheet = Image.new("RGB", (W, len(rows_) * (th + gap) + gap), rgb(C["paper"]))
|
||||
y = gap
|
||||
for r in rows_:
|
||||
x = gap
|
||||
for t in r:
|
||||
sheet.paste(t, (x, y))
|
||||
x += t.width + gap
|
||||
y += th + gap
|
||||
sheet.save(OUT / "checks" / "family.jpg", quality=76, optimize=True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
logos()
|
||||
icons()
|
||||
templates()
|
||||
report_mark()
|
||||
write(OUT / "templates" / "cli_banner.txt", CLI_BANNER.lstrip("\n"))
|
||||
table = checks()
|
||||
featured = sorted((ROOT / "blog" / "images").glob("*.jpg"))
|
||||
family(featured)
|
||||
lines = ["| colour | on | use | contrast | AA text (4.5) | graphics (3.0) |", "|---|---|---|---|---|---|"]
|
||||
for a, b, use, cr in table:
|
||||
lines.append(f"| {a} {C[a]} | {b} {C[b]} | {use} | {cr:.2f} | {'yes' if cr >= 4.5 else 'no'} | "
|
||||
f"{'yes' if cr >= 3 else 'no'} |") # fmt: skip
|
||||
write(OUT / "checks" / "contrast.md", "\n".join(lines) + "\n")
|
||||
print("\n".join(lines))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user