blog: featured images (1200x630, ~90 KB) and a branding brief
Dotted simurg on a bead grid in the channel colors (red/green, gold bird, indigo), one motif per post; generated by scripts/blog/make_featured_images.py.
This commit is contained in:
1 parent
4ab227e909
commit
ba38be122e
8 files changed
+346
No files matched your search
@@ -7,3 +7,7 @@ Five posts about why SimurgArray exists and where it is going.
|
|||||||
3. [What good could it do?](03_what_good_it_could_do.md)
|
3. [What good could it do?](03_what_good_it_could_do.md)
|
||||||
4. [Going public: what it takes, and a calendar](04_going_public_plan_and_calendar.md)
|
4. [Going public: what it takes, and a calendar](04_going_public_plan_and_calendar.md)
|
||||||
5. [Using SimurgArray, and the bird behind the name](05_using_simurgarray_and_the_bird_behind_the_name.md)
|
5. [Using SimurgArray, and the bird behind the name](05_using_simurgarray_and_the_bird_behind_the_name.md)
|
||||||
|
|
||||||
|
Featured images (1200 x 630 JPEG, about 90 KB each) are in `images/`, one per post with the same file name.
|
||||||
|
They are drawn by `scripts/blog/make_featured_images.py` (Pillow), so they can be regenerated or restyled.
|
||||||
|
`branding_prompt.md` is a brief for designing the full brand identity.
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# Branding brief for SimurgArray
|
||||||
|
|
||||||
|
Copy everything below the line into the branding agent.
|
||||||
|
|
||||||
|
________________________________________________________________________________
|
||||||
|
|
||||||
|
You are a senior brand and visual identity designer. Build a complete, small, practical brand identity for an open source scientific software project called **SimurgArray**. Work carefully, explain your choices briefly, and deliver production ready files.
|
||||||
|
|
||||||
|
## The project in one paragraph
|
||||||
|
|
||||||
|
SimurgArray is a personal, non commercial research toolkit for Illumina Infinium genotyping arrays. It turns raw scanner files (IDATs) into genotype calls, quality control reports, VCF and PLINK files, and goes on to copy number, loss of heterozygosity, mosaicism, pharmacogenomics and training its own cluster files. It reproduces the vendor's genotype files byte for byte on public data, runs on a Raspberry Pi, and documents every place where it differs from reference tools. It is MIT licensed and about to go public. It is a research tool, never a medical or diagnostic product.
|
||||||
|
|
||||||
|
## The name and the story
|
||||||
|
|
||||||
|
In Persian and Turkic mythology the Simurg is a great, wise bird. In Attar's twelfth century poem *The Conference of the Birds*, thirty birds travel to find the Simurg and discover that "si murg" means "thirty birds": the Simurg was all of them together. An array works the same way: hundreds of thousands of tiny beads only mean something when read together. By coincidence, the public demo dataset used in validation turned out to be exactly thirty people. The brand should carry this idea quietly, never explain it loudly.
|
||||||
|
|
||||||
|
Tagline: **"Thirty birds, one picture."**
|
||||||
|
|
||||||
|
## Audience
|
||||||
|
|
||||||
|
Bioinformaticians, genetic epidemiologists, lab scientists running arrays, students, biobanks with small budgets. They are skeptical, detail oriented and allergic to marketing. They trust numbers, clear limitations and good documentation.
|
||||||
|
|
||||||
|
## Personality
|
||||||
|
|
||||||
|
Precise, calm, honest, a little dry, quietly clever. Think of a well made scientific instrument with a small smile engraved somewhere nobody asked for one. Not corporate, not playful, not futuristic, not "AI".
|
||||||
|
|
||||||
|
## Existing visual direction (keep and refine it, do not replace it)
|
||||||
|
|
||||||
|
* **Core motif:** a bird in flight drawn only with round dots on a square grid, like beads on a BeadChip. The current bird faces right, wings raised and swept back, with a small crest, a hooked beak and three long tail plumes ending in round "peacock eye" dots.
|
||||||
|
* **Channel colors:** the scanner has a red and a green channel. Dots on the outline of the bird are red and green; the inner dots, where both channels "overlap", are gold.
|
||||||
|
* **Palette (starting point, you may tune values for contrast):**
|
||||||
|
* Channel red #C8453A
|
||||||
|
* Channel green #2F8F5B
|
||||||
|
* Simurg gold #D4A017
|
||||||
|
* Night indigo #1E2240 (backgrounds; most of the work happened at night)
|
||||||
|
* Paper #F7F5EF (documents, light backgrounds)
|
||||||
|
* **Type so far:** Noto Sans for text, Noto Sans Mono for anything a user might paste into a terminal.
|
||||||
|
* Five blog featured images already exist in this style (1200 x 630, indigo background, faint bead grid, bird on the right, title on the left, footer with three colored dots, the name and the tagline). Stay consistent with them.
|
||||||
|
|
||||||
|
## Deliverables
|
||||||
|
|
||||||
|
1. **Logo system**
|
||||||
|
* Primary mark: the dotted bird, redrawn so it reads clearly as a bird at every size. Provide a detailed version (roughly 30 to 40 dots across) and a simplified version (roughly 8 to 12 dots across).
|
||||||
|
* Wordmark "SimurgArray" and a lockup of mark plus wordmark, horizontal and stacked.
|
||||||
|
* Favicon and app icon: 16, 32, 48, 180 and 512 px. At 16 px the bird may become an abstract cluster of dots, but it must still feel like the same family.
|
||||||
|
* Versions for dark background, light background, single color gold, single color black and single color white.
|
||||||
|
* Formats: SVG (clean, no embedded bitmaps, dots as circles) and PNG exports.
|
||||||
|
2. **Color system:** final hex values, usage ratios, and a contrast table showing that text colors meet WCAG AA on indigo and on paper. Include a rule for charts: red and green are the channel colors, so every chart must also be readable for red green color blind viewers (use shape, position or a blue alternative, and say how).
|
||||||
|
3. **Typography:** confirm or replace Noto Sans and Noto Sans Mono with free, open licensed fonts. Give sizes and weights for headings, body text, code and report tables (tabular figures for numbers).
|
||||||
|
4. **Dot grid system:** the grid spacing, dot sizes and rules for building new illustrations from dots, so other people can draw consistent images later.
|
||||||
|
5. **Templates**
|
||||||
|
* WordPress featured image, 1200 x 630.
|
||||||
|
* Social avatar, square, readable when shown as a circle.
|
||||||
|
* GitHub README banner, around 1280 x 320.
|
||||||
|
* Social preview for the repository, 1280 x 640.
|
||||||
|
* A small corner mark for HTML QC reports, together with the rule that the software version and a run checksum always appear next to it.
|
||||||
|
* An optional ASCII or Unicode banner for the command line, at most 6 lines and 60 columns, that still looks good in a plain terminal.
|
||||||
|
6. **Short brand guide** (two to four pages, Markdown or PDF): the story in three sentences, voice rules, do and do not examples for the logo, color and type specs, and how to build a new featured image.
|
||||||
|
|
||||||
|
## Voice rules for any text you write
|
||||||
|
|
||||||
|
* Plain, exact English. Short sentences. Numbers instead of adjectives.
|
||||||
|
* Every claim sits next to its limitation.
|
||||||
|
* Humor is allowed only as a faint smile, never as a joke that needs explaining.
|
||||||
|
* **Do not use em dashes, en dashes or hyphens in prose.** Rephrase instead ("open source", "non commercial", "step by step"). Hyphens inside code, file names and command options are fine.
|
||||||
|
* Never use: "revolutionary", "cutting edge", "AI powered", "next generation", "seamless", "unlock".
|
||||||
|
|
||||||
|
## Hard constraints
|
||||||
|
|
||||||
|
* Do not use, imitate or reference the logos, colors or product names of Illumina or any other genomics company. Illumina and Infinium may appear only as plain words describing compatibility, never as visual elements.
|
||||||
|
* Do not name or allude to the vendor's commercial analysis software products.
|
||||||
|
* No medical imagery: no crosses, pills, stethoscopes or DNA helices used as clinical symbols. A double helix, if used at all, must be abstract and secondary. The bird is the hero.
|
||||||
|
* No stock photos, no gradients that mimic "tech startup" style, no 3D renders.
|
||||||
|
* Everything must be original and free to publish under the project's MIT license or CC BY 4.0 for artwork. List any fonts or assets you use with their licenses.
|
||||||
|
* Files must be small: SVGs clean and optimized, PNG and JPEG exports under 150 KB each where possible.
|
||||||
|
|
||||||
|
## Acceptance checks (report on each)
|
||||||
|
|
||||||
|
1. The mark is recognizable as a bird at 48 px and still distinctive at 16 px.
|
||||||
|
2. Dark and light versions both pass WCAG AA for any text.
|
||||||
|
3. A red green color blind simulation of the logo and one chart example still reads well.
|
||||||
|
4. The five existing featured images and your new templates look like one family when placed side by side.
|
||||||
|
5. No prose you wrote contains an em dash, en dash or hyphen.
|
||||||
|
|
||||||
|
Start by restating the brief in five bullet points, list any questions you would normally ask a client (and the assumption you will make for each), then produce the deliverables.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
@@ -0,0 +1,257 @@
|
|||||||
|
"""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.
|
||||||
|
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
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
||||||
|
|
||||||
|
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 _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)
|
||||||
|
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 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)))
|
||||||
|
d = ImageDraw.Draw(img)
|
||||||
|
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))
|
||||||
|
return pts
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
elif kind == "converge": # dots flowing in from the edges
|
||||||
|
pts = dot_bird(img, bx, by, bw, bh, 13 * SS, rng)
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
elif kind == "calendar": # a calendar grid under the bird, some weeks done
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
Reference in new issue
Block a user