diff --git a/blog/images/01_why_simurgarray_should_exist.jpg b/blog/images/01_why_simurgarray_should_exist.jpg index 1d71a37..ecf22d3 100644 Binary files a/blog/images/01_why_simurgarray_should_exist.jpg and b/blog/images/01_why_simurgarray_should_exist.jpg differ diff --git a/blog/images/02_alternatives_and_why_i_built_it_anyway.jpg b/blog/images/02_alternatives_and_why_i_built_it_anyway.jpg index 8b61bb7..cd4cb6b 100644 Binary files a/blog/images/02_alternatives_and_why_i_built_it_anyway.jpg and b/blog/images/02_alternatives_and_why_i_built_it_anyway.jpg differ diff --git a/blog/images/03_what_good_it_could_do.jpg b/blog/images/03_what_good_it_could_do.jpg index f78e59c..474d15f 100644 Binary files a/blog/images/03_what_good_it_could_do.jpg and b/blog/images/03_what_good_it_could_do.jpg differ diff --git a/blog/images/04_going_public_plan_and_calendar.jpg b/blog/images/04_going_public_plan_and_calendar.jpg index 2ce296c..13640ee 100644 Binary files a/blog/images/04_going_public_plan_and_calendar.jpg and b/blog/images/04_going_public_plan_and_calendar.jpg differ diff --git a/blog/images/05_using_simurgarray_and_the_bird_behind_the_name.jpg b/blog/images/05_using_simurgarray_and_the_bird_behind_the_name.jpg index 6a465f8..4330421 100644 Binary files a/blog/images/05_using_simurgarray_and_the_bird_behind_the_name.jpg and b/blog/images/05_using_simurgarray_and_the_bird_behind_the_name.jpg differ diff --git a/branding/README.md b/branding/README.md new file mode 100644 index 0000000..3293e24 --- /dev/null +++ b/branding/README.md @@ -0,0 +1,171 @@ +# SimurgArray brand guide + +Everything here is generated by `scripts/brand/make_brand.py` (logos, icons, templates, checks) and +`scripts/blog/make_featured_images.py` (blog images). The mark geometry lives in one place, +`scripts/brand/simurg_mark.py`, so every file shows exactly the same dots. Artwork is released under CC BY 4.0; the +fonts are Noto Sans and Noto Sans Mono (SIL Open Font License 1.1). + +![Horizontal lockup](logo/png/lockup_horizontal_dark.png) + +## The story in three sentences + +In Attar's *Conference of the Birds*, thirty birds search for the Simurg and find that "si murg" means thirty birds: +the Simurg was all of them together. A genotyping array is the same idea, hundreds of thousands of small beads that +only mean something when read together. SimurgArray is the tool that reads them together, openly. + +Tagline: **Thirty birds, one picture.** + +## The mark + +A simurg seen from the front, wings raised, drawn only with round dots on a square grid, the way beads sit on a +BeadChip. + +* **Gold body, wings and crown.** Gold is what you get when the two scanner channels meet. +* **Three tail plumes:** red on the left (the red channel), gold in the middle, green on the right (the green + channel). Each ends in a round peacock eye. +* **Three dots on the head** form a small crown. Nobody has to notice it. + +Three drawings of the same bird, chosen by size: + +* `mark` (31 x 27 dots): 100 px and larger, banners, lockups, print. +* `mark_simple` (11 x 10 dots): 24 to 100 px, report corners, small UI. +* `mark_micro` (7 x 7 dots): 16 px favicon only. + +Colour versions of each, in `logo/` as SVG and in `logo/png/` as PNG: + +* `dark`: gold #D4A017 with red and green plumes, for indigo or other dark backgrounds. +* `light`: deep gold #A87A00 with red and green plumes, for paper or white. +* `gold`, `black`, `white`: single colour, for stamps, embossing, merchandise, fax machines that still exist. + +The SVGs contain only circles, no embedded images and no fonts, so they render the same everywhere. + +### Wordmark and lockups + +"Simurg" in the text colour and "Array" in the accent colour, Noto Sans SemiBold, outlined to paths so no font is +needed. On dark backgrounds the accent is gold #D4A017, on light backgrounds gold ink #8C6500. + +* `lockup_horizontal_*`: mark left, wordmark right. The default. +* `lockup_stacked_*`: mark above, wordmark centred below. For square spaces. + +Clear space around any lockup: at least the height of the mark's head (about three dot rows) on every side. + +### Do + +* Keep the dots round and on their grid. +* Keep the red plume on the left and the green plume on the right. +* Use the light version on light backgrounds; plain gold on paper is too faint (contrast 2.18). +* Scale the whole mark; pick the simple or micro drawing when the dots would get smaller than about 2 px. + +### Do not + +* Recolour the plumes, add gradients, shadows, glows or outlines. +* Turn dots into squares or connect them with lines. +* Rotate, mirror or stretch the mark. +* Put the mark on busy photos. +* Combine it with DNA helices, crosses, pills or any clinical imagery. SimurgArray is a research tool. +* Use the logos, colours or product names of any genomics company as visual elements. Illumina and Infinium appear + only as plain words describing compatibility. + +## Colour + +Brand colours: + +* **Night indigo** #1E2240: backgrounds (most of the work happened at night). Deep indigo #12142A for gradients. +* **Paper** #F7F5EF: light backgrounds and text on dark. +* **Simurg gold** #D4A017: the bird, accents on dark. +* **Gold deep** #A87A00: the bird on light backgrounds. +* **Gold ink** #8C6500: accent text on light backgrounds. +* **Channel red** #C8453A and **channel green** #2F8F5B: the plumes and the bead grid only. Never for text. +* **Muted on dark** #A3A7C9 and **muted on light** #5A5E7E: secondary text. + +Rough proportions on a dark page: indigo 80, paper 10, gold 7, red and green 3 together. + +Every text colour passes WCAG AA (4.5) and every graphic colour passes 3.0 on its background. The full table with +the computed ratios is in `checks/contrast.md`. + +### Charts + +Red against green is exactly the pair that red green colour blind readers cannot separate, and in genotyping it is +also the most tempting pair. So charts never use the channel colours to tell data apart. Use the Okabe and Ito +palette with a second cue: + +* AA: vermillion #D55E00, circles. +* AB: bluish green #009E73, triangles. +* BB: blue #0072B2, squares. + +Always add shape, position or a direct label as well as colour. `checks/colour_vision_check.png` shows the mark and +an example cluster plot under simulated protanopia and deuteranopia (Machado et al. 2009). The bird stays readable +because its shape carries it; the two coloured plumes become similar but keep their left and right positions; the +chart stays readable because of the shapes. + +## Type + +* **Noto Sans** for everything people read. Headings SemiBold or Bold, body Regular. +* **Noto Sans Mono** for anything a user might paste into a terminal, file names, and small labels such as + "PART 3". +* Numbers in tables use tabular figures (`font-variant-numeric: tabular-nums`), so columns of call rates line up. + +Suggested sizes for the web: body 17 px with 1.6 line height, H1 40 px, H2 28 px, H3 21 px, code 15 px, table text +15 px. Reports: 13 px tables, 12 px captions. + +## The dot grid + +New illustrations are built from the same material as the logo. + +* Background grid: dots every 18 px (at 1200 px width), radius 1.6 px, alternating red and green at 14 % opacity + over the indigo gradient. +* Foreground dots: radius 40 % of the cell, so neighbours never touch. +* Colour by meaning: gold for the subject, red and green only for things that come from the two channels, dimmed + gold or indigo mixes for "background" subjects. +* Motion is suggested with trails of shrinking, fading dots, never with lines. +* One subject per image. Empty space is part of the grid. + +## Templates + +In `templates/`: + +* `featured_template_1200x630.jpg`: WordPress featured image and social preview for posts. Title on the left in Noto + Sans Bold (58 px, shrinks to fit a 540 px column), "PART N" above it in gold mono, footer with three dots, the name + and the tagline. The five blog images in `blog/images/` follow this layout, each with its own small motif. +* `social_preview_1280x640.png`: the repository preview image. +* `readme_banner_1280x320.png`: the top of the README. +* `avatar_512.png`: square avatar; the mark sits inside the central circle, so round crops are safe. +* `report_corner_mark.svg` and `report_corner_mark.html`: the small mark for HTML QC reports. It is always shown with + the software version and the first 12 characters of the run manifest checksum. The bird is for recognition; the + checksum is what actually proves anything. +* `cli_banner.txt`: six lines, 46 columns, plain ASCII, for `simurg --version` or a first run. + +Icons in `icons/`: `favicon.ico` (16, 32, 48), PNGs at 16, 32, 48, 180 (Apple touch) and 512, all on a rounded +indigo tile. `checks/icon_sizes.png` shows them side by side. + +## Making a new featured image + +1. Add the post to `POSTS` in `scripts/blog/make_featured_images.py`: file stem, "Part N", the title with `\n` + where lines should break, and a motif. +2. Reuse a motif or write a new one: draw the mark with `draw_mark`, add at most one supporting element. +3. Run `python3 scripts/blog/make_featured_images.py`. Files come out at 1200 x 630 and about 100 KB. + +## Voice + +* Plain, exact English. Short sentences. Numbers instead of adjectives. +* Every claim sits next to its limitation, in the same paragraph. +* Humour only as a faint smile. +* No em dashes, en dashes or hyphens in prose; rephrase instead. Hyphens in code, file names and options are fine. +* Never: revolutionary, cutting edge, AI powered, next generation, seamless, unlock. + +Examples: + +* Yes: "Concordance with 30x sequencing was 99.44 %, against 99.46 % for the vendor's cluster file." +* No: "Best in class accuracy that rivals commercial solutions." +* Yes: "Research use only. The PGx report shows the evidence behind every call." +* No: "Personalised medicine, unlocked." + +## Acceptance checks + +1. **Recognisable at 48 px, distinctive at 16 px:** see `checks/icon_sizes.png`. The simple and micro drawings were + drawn by hand for these sizes instead of scaling the detailed one down. +2. **Contrast:** all text pairs pass AA, all graphic pairs pass 3.0 (`checks/contrast.md`); plain gold on paper is + the one pair that fails, and the guide forbids it. +3. **Colour vision:** `checks/colour_vision_check.png`. +4. **One family:** `checks/family.jpg` puts the five blog images and the templates side by side. +5. **No dashes in prose:** this guide and all generated text were checked for em dashes, en dashes and hyphens. diff --git a/branding/checks/colour_vision_check.png b/branding/checks/colour_vision_check.png new file mode 100644 index 0000000..7d66ee6 Binary files /dev/null and b/branding/checks/colour_vision_check.png differ diff --git a/branding/checks/contrast.md b/branding/checks/contrast.md new file mode 100644 index 0000000..64aacdd --- /dev/null +++ b/branding/checks/contrast.md @@ -0,0 +1,14 @@ +| colour | on | use | contrast | AA text (4.5) | graphics (3.0) | +|---|---|---|---|---|---| +| paper #F7F5EF | indigo #1E2240 | text on dark | 14.18 | yes | yes | +| muted_dark #A3A7C9 | indigo #1E2240 | secondary text on dark | 6.57 | yes | yes | +| gold #D4A017 | indigo #1E2240 | accent text on dark | 6.51 | yes | yes | +| indigo #1E2240 | paper #F7F5EF | text on light | 14.18 | yes | yes | +| muted_light #5A5E7E | paper #F7F5EF | secondary text on light | 5.77 | yes | yes | +| gold_ink #8C6500 | paper #F7F5EF | accent text on light | 4.84 | yes | yes | +| red #C8453A | indigo #1E2240 | graphics on dark | 3.22 | no | yes | +| green #2F8F5B | indigo #1E2240 | graphics on dark | 3.83 | no | yes | +| gold_deep #A87A00 | paper #F7F5EF | graphics on light | 3.54 | no | yes | +| red #C8453A | paper #F7F5EF | graphics on light | 4.41 | no | yes | +| green #2F8F5B | paper #F7F5EF | graphics on light | 3.70 | no | yes | +| gold #D4A017 | paper #F7F5EF | never for text or small marks | 2.18 | no | no | diff --git a/branding/checks/family.jpg b/branding/checks/family.jpg new file mode 100644 index 0000000..71806c9 Binary files /dev/null and b/branding/checks/family.jpg differ diff --git a/branding/checks/icon_sizes.png b/branding/checks/icon_sizes.png new file mode 100644 index 0000000..ebaaa1b Binary files /dev/null and b/branding/checks/icon_sizes.png differ diff --git a/branding/icons/favicon.ico b/branding/icons/favicon.ico new file mode 100644 index 0000000..bf7c901 Binary files /dev/null and b/branding/icons/favicon.ico differ diff --git a/branding/icons/icon_16.png b/branding/icons/icon_16.png new file mode 100644 index 0000000..b29a0c0 Binary files /dev/null and b/branding/icons/icon_16.png differ diff --git a/branding/icons/icon_16.svg b/branding/icons/icon_16.svg new file mode 100644 index 0000000..0d9b13f --- /dev/null +++ b/branding/icons/icon_16.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/icons/icon_180.png b/branding/icons/icon_180.png new file mode 100644 index 0000000..376f5c7 Binary files /dev/null and b/branding/icons/icon_180.png differ diff --git a/branding/icons/icon_32.png b/branding/icons/icon_32.png new file mode 100644 index 0000000..db4680b Binary files /dev/null and b/branding/icons/icon_32.png differ diff --git a/branding/icons/icon_48.png b/branding/icons/icon_48.png new file mode 100644 index 0000000..6e098d8 Binary files /dev/null and b/branding/icons/icon_48.png differ diff --git a/branding/icons/icon_512.png b/branding/icons/icon_512.png new file mode 100644 index 0000000..9a4fe38 Binary files /dev/null and b/branding/icons/icon_512.png differ diff --git a/branding/icons/icon_512.svg b/branding/icons/icon_512.svg new file mode 100644 index 0000000..5dc329c --- /dev/null +++ b/branding/icons/icon_512.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/lockup_horizontal_black.svg b/branding/logo/lockup_horizontal_black.svg new file mode 100644 index 0000000..59be904 --- /dev/null +++ b/branding/logo/lockup_horizontal_black.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/lockup_horizontal_dark.svg b/branding/logo/lockup_horizontal_dark.svg new file mode 100644 index 0000000..90526fa --- /dev/null +++ b/branding/logo/lockup_horizontal_dark.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/lockup_horizontal_light.svg b/branding/logo/lockup_horizontal_light.svg new file mode 100644 index 0000000..c40b605 --- /dev/null +++ b/branding/logo/lockup_horizontal_light.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/lockup_horizontal_white.svg b/branding/logo/lockup_horizontal_white.svg new file mode 100644 index 0000000..4c4300b --- /dev/null +++ b/branding/logo/lockup_horizontal_white.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/lockup_stacked_black.svg b/branding/logo/lockup_stacked_black.svg new file mode 100644 index 0000000..0843858 --- /dev/null +++ b/branding/logo/lockup_stacked_black.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/lockup_stacked_dark.svg b/branding/logo/lockup_stacked_dark.svg new file mode 100644 index 0000000..9b4ebeb --- /dev/null +++ b/branding/logo/lockup_stacked_dark.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/lockup_stacked_light.svg b/branding/logo/lockup_stacked_light.svg new file mode 100644 index 0000000..9bdc01d --- /dev/null +++ b/branding/logo/lockup_stacked_light.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/lockup_stacked_white.svg b/branding/logo/lockup_stacked_white.svg new file mode 100644 index 0000000..80a5609 --- /dev/null +++ b/branding/logo/lockup_stacked_white.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/mark_black.svg b/branding/logo/mark_black.svg new file mode 100644 index 0000000..72b087a --- /dev/null +++ b/branding/logo/mark_black.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/mark_dark.svg b/branding/logo/mark_dark.svg new file mode 100644 index 0000000..e6309ba --- /dev/null +++ b/branding/logo/mark_dark.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/mark_gold.svg b/branding/logo/mark_gold.svg new file mode 100644 index 0000000..0fc89db --- /dev/null +++ b/branding/logo/mark_gold.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/mark_light.svg b/branding/logo/mark_light.svg new file mode 100644 index 0000000..140f091 --- /dev/null +++ b/branding/logo/mark_light.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/mark_micro_black.svg b/branding/logo/mark_micro_black.svg new file mode 100644 index 0000000..1a56e8f --- /dev/null +++ b/branding/logo/mark_micro_black.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/mark_micro_dark.svg b/branding/logo/mark_micro_dark.svg new file mode 100644 index 0000000..e9b0f5c --- /dev/null +++ b/branding/logo/mark_micro_dark.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/mark_micro_gold.svg b/branding/logo/mark_micro_gold.svg new file mode 100644 index 0000000..1569708 --- /dev/null +++ b/branding/logo/mark_micro_gold.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/mark_micro_light.svg b/branding/logo/mark_micro_light.svg new file mode 100644 index 0000000..c82bea8 --- /dev/null +++ b/branding/logo/mark_micro_light.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/mark_micro_white.svg b/branding/logo/mark_micro_white.svg new file mode 100644 index 0000000..74db5b4 --- /dev/null +++ b/branding/logo/mark_micro_white.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/mark_simple_black.svg b/branding/logo/mark_simple_black.svg new file mode 100644 index 0000000..b5f6b39 --- /dev/null +++ b/branding/logo/mark_simple_black.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/mark_simple_dark.svg b/branding/logo/mark_simple_dark.svg new file mode 100644 index 0000000..092c95a --- /dev/null +++ b/branding/logo/mark_simple_dark.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/mark_simple_gold.svg b/branding/logo/mark_simple_gold.svg new file mode 100644 index 0000000..c2c5b48 --- /dev/null +++ b/branding/logo/mark_simple_gold.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/mark_simple_light.svg b/branding/logo/mark_simple_light.svg new file mode 100644 index 0000000..7f1d731 --- /dev/null +++ b/branding/logo/mark_simple_light.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/mark_simple_white.svg b/branding/logo/mark_simple_white.svg new file mode 100644 index 0000000..d75c42c --- /dev/null +++ b/branding/logo/mark_simple_white.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/mark_white.svg b/branding/logo/mark_white.svg new file mode 100644 index 0000000..cdda24b --- /dev/null +++ b/branding/logo/mark_white.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/png/lockup_horizontal_black.png b/branding/logo/png/lockup_horizontal_black.png new file mode 100644 index 0000000..780125e Binary files /dev/null and b/branding/logo/png/lockup_horizontal_black.png differ diff --git a/branding/logo/png/lockup_horizontal_dark.png b/branding/logo/png/lockup_horizontal_dark.png new file mode 100644 index 0000000..ef07987 Binary files /dev/null and b/branding/logo/png/lockup_horizontal_dark.png differ diff --git a/branding/logo/png/lockup_horizontal_light.png b/branding/logo/png/lockup_horizontal_light.png new file mode 100644 index 0000000..df5769d Binary files /dev/null and b/branding/logo/png/lockup_horizontal_light.png differ diff --git a/branding/logo/png/lockup_horizontal_white.png b/branding/logo/png/lockup_horizontal_white.png new file mode 100644 index 0000000..6c0039f Binary files /dev/null and b/branding/logo/png/lockup_horizontal_white.png differ diff --git a/branding/logo/png/lockup_stacked_black.png b/branding/logo/png/lockup_stacked_black.png new file mode 100644 index 0000000..1e57890 Binary files /dev/null and b/branding/logo/png/lockup_stacked_black.png differ diff --git a/branding/logo/png/lockup_stacked_dark.png b/branding/logo/png/lockup_stacked_dark.png new file mode 100644 index 0000000..19cd310 Binary files /dev/null and b/branding/logo/png/lockup_stacked_dark.png differ diff --git a/branding/logo/png/lockup_stacked_light.png b/branding/logo/png/lockup_stacked_light.png new file mode 100644 index 0000000..c967fef Binary files /dev/null and b/branding/logo/png/lockup_stacked_light.png differ diff --git a/branding/logo/png/lockup_stacked_white.png b/branding/logo/png/lockup_stacked_white.png new file mode 100644 index 0000000..dc9031c Binary files /dev/null and b/branding/logo/png/lockup_stacked_white.png differ diff --git a/branding/logo/png/mark_black.png b/branding/logo/png/mark_black.png new file mode 100644 index 0000000..31ff153 Binary files /dev/null and b/branding/logo/png/mark_black.png differ diff --git a/branding/logo/png/mark_dark.png b/branding/logo/png/mark_dark.png new file mode 100644 index 0000000..9f3fd86 Binary files /dev/null and b/branding/logo/png/mark_dark.png differ diff --git a/branding/logo/png/mark_gold.png b/branding/logo/png/mark_gold.png new file mode 100644 index 0000000..e39dbde Binary files /dev/null and b/branding/logo/png/mark_gold.png differ diff --git a/branding/logo/png/mark_light.png b/branding/logo/png/mark_light.png new file mode 100644 index 0000000..68a1c3d Binary files /dev/null and b/branding/logo/png/mark_light.png differ diff --git a/branding/logo/png/mark_micro_black.png b/branding/logo/png/mark_micro_black.png new file mode 100644 index 0000000..44225ba Binary files /dev/null and b/branding/logo/png/mark_micro_black.png differ diff --git a/branding/logo/png/mark_micro_dark.png b/branding/logo/png/mark_micro_dark.png new file mode 100644 index 0000000..a1804b3 Binary files /dev/null and b/branding/logo/png/mark_micro_dark.png differ diff --git a/branding/logo/png/mark_micro_gold.png b/branding/logo/png/mark_micro_gold.png new file mode 100644 index 0000000..e8724c9 Binary files /dev/null and b/branding/logo/png/mark_micro_gold.png differ diff --git a/branding/logo/png/mark_micro_light.png b/branding/logo/png/mark_micro_light.png new file mode 100644 index 0000000..1a8c8e5 Binary files /dev/null and b/branding/logo/png/mark_micro_light.png differ diff --git a/branding/logo/png/mark_micro_white.png b/branding/logo/png/mark_micro_white.png new file mode 100644 index 0000000..1ec261f Binary files /dev/null and b/branding/logo/png/mark_micro_white.png differ diff --git a/branding/logo/png/mark_simple_black.png b/branding/logo/png/mark_simple_black.png new file mode 100644 index 0000000..087eb2d Binary files /dev/null and b/branding/logo/png/mark_simple_black.png differ diff --git a/branding/logo/png/mark_simple_dark.png b/branding/logo/png/mark_simple_dark.png new file mode 100644 index 0000000..4b0fec7 Binary files /dev/null and b/branding/logo/png/mark_simple_dark.png differ diff --git a/branding/logo/png/mark_simple_gold.png b/branding/logo/png/mark_simple_gold.png new file mode 100644 index 0000000..4b93e47 Binary files /dev/null and b/branding/logo/png/mark_simple_gold.png differ diff --git a/branding/logo/png/mark_simple_light.png b/branding/logo/png/mark_simple_light.png new file mode 100644 index 0000000..17f827d Binary files /dev/null and b/branding/logo/png/mark_simple_light.png differ diff --git a/branding/logo/png/mark_simple_white.png b/branding/logo/png/mark_simple_white.png new file mode 100644 index 0000000..efb0f6d Binary files /dev/null and b/branding/logo/png/mark_simple_white.png differ diff --git a/branding/logo/png/mark_white.png b/branding/logo/png/mark_white.png new file mode 100644 index 0000000..d3518ef Binary files /dev/null and b/branding/logo/png/mark_white.png differ diff --git a/branding/logo/png/wordmark_black.png b/branding/logo/png/wordmark_black.png new file mode 100644 index 0000000..d1fa61a Binary files /dev/null and b/branding/logo/png/wordmark_black.png differ diff --git a/branding/logo/png/wordmark_dark.png b/branding/logo/png/wordmark_dark.png new file mode 100644 index 0000000..9be8916 Binary files /dev/null and b/branding/logo/png/wordmark_dark.png differ diff --git a/branding/logo/png/wordmark_gold.png b/branding/logo/png/wordmark_gold.png new file mode 100644 index 0000000..bd7c855 Binary files /dev/null and b/branding/logo/png/wordmark_gold.png differ diff --git a/branding/logo/png/wordmark_light.png b/branding/logo/png/wordmark_light.png new file mode 100644 index 0000000..9826252 Binary files /dev/null and b/branding/logo/png/wordmark_light.png differ diff --git a/branding/logo/png/wordmark_white.png b/branding/logo/png/wordmark_white.png new file mode 100644 index 0000000..a50b254 Binary files /dev/null and b/branding/logo/png/wordmark_white.png differ diff --git a/branding/logo/wordmark_black.svg b/branding/logo/wordmark_black.svg new file mode 100644 index 0000000..5a41587 --- /dev/null +++ b/branding/logo/wordmark_black.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/wordmark_dark.svg b/branding/logo/wordmark_dark.svg new file mode 100644 index 0000000..1364c75 --- /dev/null +++ b/branding/logo/wordmark_dark.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/wordmark_gold.svg b/branding/logo/wordmark_gold.svg new file mode 100644 index 0000000..f9ab581 --- /dev/null +++ b/branding/logo/wordmark_gold.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/wordmark_light.svg b/branding/logo/wordmark_light.svg new file mode 100644 index 0000000..23db83f --- /dev/null +++ b/branding/logo/wordmark_light.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/logo/wordmark_white.svg b/branding/logo/wordmark_white.svg new file mode 100644 index 0000000..444cedb --- /dev/null +++ b/branding/logo/wordmark_white.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/templates/avatar_512.png b/branding/templates/avatar_512.png new file mode 100644 index 0000000..bb59681 Binary files /dev/null and b/branding/templates/avatar_512.png differ diff --git a/branding/templates/cli_banner.txt b/branding/templates/cli_banner.txt new file mode 100644 index 0000000..8906e40 --- /dev/null +++ b/branding/templates/cli_banner.txt @@ -0,0 +1,6 @@ + o o SimurgArray {version} + oo o oo Thirty birds, one picture. + ooooooo + ooo IDAT to genotypes, QC, CN, PGx + / | \ research use only + * o * diff --git a/branding/templates/featured_template_1200x630.jpg b/branding/templates/featured_template_1200x630.jpg new file mode 100644 index 0000000..cd56a4f Binary files /dev/null and b/branding/templates/featured_template_1200x630.jpg differ diff --git a/branding/templates/readme_banner_1280x320.png b/branding/templates/readme_banner_1280x320.png new file mode 100644 index 0000000..2416367 Binary files /dev/null and b/branding/templates/readme_banner_1280x320.png differ diff --git a/branding/templates/report_corner_mark.html b/branding/templates/report_corner_mark.html new file mode 100644 index 0000000..211d6f8 --- /dev/null +++ b/branding/templates/report_corner_mark.html @@ -0,0 +1,6 @@ + +
+ SimurgArray + SimurgArray {version} · run {manifest_sha256[:12]} +
diff --git a/branding/templates/report_corner_mark.svg b/branding/templates/report_corner_mark.svg new file mode 100644 index 0000000..092c95a --- /dev/null +++ b/branding/templates/report_corner_mark.svg @@ -0,0 +1 @@ +SimurgArray diff --git a/branding/templates/social_preview_1280x640.png b/branding/templates/social_preview_1280x640.png new file mode 100644 index 0000000..4189faa Binary files /dev/null and b/branding/templates/social_preview_1280x640.png differ diff --git a/scripts/blog/make_featured_images.py b/scripts/blog/make_featured_images.py index 1c02062..1356df9 100644 --- a/scripts/blog/make_featured_images.py +++ b/scripts/blog/make_featured_images.py @@ -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: diff --git a/scripts/brand/make_brand.py b/scripts/brand/make_brand.py new file mode 100644 index 0000000..7195943 --- /dev/null +++ b/scripts/brand/make_brand.py @@ -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'' + for c, r in cells] # fmt: skip + if dots: + out.append(f'' + "".join(dots) + "") + return "".join(out) + + +def svg(w: float, h: float, body: str, bg: str | None = None, title: str = "SimurgArray") -> str: + rect = f'' if bg else "" + return (f'{title}{rect}{body}\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'' + 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'' + 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 + SimurgArray {{version}} · run {{manifest_sha256[:12]}} +
+""") # 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() diff --git a/scripts/brand/simurg_mark.py b/scripts/brand/simurg_mark.py new file mode 100644 index 0000000..efb2ee1 --- /dev/null +++ b/scripts/brand/simurg_mark.py @@ -0,0 +1,86 @@ +"""Geometry of the SimurgArray mark on its dot grid (no dependencies beyond the standard library). + +A frontal simurg with raised wings (three feathers each), a three-dot crown and three tail plumes: red channel, +gold, green channel, each ending in a peacock eye. Shapes are capsules, circles and an ellipse in grid units; a cell +belongs to the first shape that contains its centre. ``SIMPLE`` and ``MICRO`` are hand-drawn small versions. +""" + +from __future__ import annotations + +import math +from typing import Any + + +def _seg(px: float, py: float, a: tuple[float, float], b: tuple[float, float]) -> float: + dx, dy = b[0] - a[0], b[1] - a[1] + t = max(0.0, min(1.0, ((px - a[0]) * dx + (py - a[1]) * dy) / ((dx * dx + dy * dy) or 1.0))) + return math.hypot(px - a[0] - t * dx, py - a[1] - t * dy) + + +CX = 15.5 +_WING = [([(13.6, 9.6), (8, 5.2), (1.8, 1.4)], 1.25), ([(13.4, 11), (7.5, 8.4), (1.2, 6.6)], 1.25), + ([(13.4, 12.6), (8, 12), (2.6, 11.8)], 1.2)] # fmt: skip +SHAPES: list[tuple[str, Any, str]] = [] +for pts, r in _WING: + SHAPES.append(("cap", (pts, r), "gold")) + SHAPES.append(("cap", ([(2 * CX - x, y) for x, y in pts], r), "gold")) +SHAPES += [ + ("circ", (CX, 4.4, 2.3), "gold"), ("cap", ([(CX, 6), (CX, 8)], 1.3), "gold"), # head, neck + ("circ", (CX, 1.2, 0.62), "gold"), ("circ", (13.6, 1.9, 0.62), "gold"), ("circ", (17.4, 1.9, 0.62), "gold"), + ("ell", (CX, 12.5, 2.9, 5.4), "gold"), # body + ("cap", ([(14.8, 17), (12.6, 20.5), (10.4, 23.2)], 0.7), "red"), ("circ", (10, 24.3, 1.6), "red"), + ("cap", ([(CX, 17.5), (CX, 23.2)], 0.7), "gold"), ("circ", (CX, 24.6, 1.6), "gold"), + ("cap", ([(16.2, 17), (18.4, 20.5), (20.6, 23.2)], 0.7), "green"), ("circ", (21, 24.3, 1.6), "green"), +] # fmt: skip +DETAILED_SIZE = (31, 27) +SIMPLE = """ +X.........X +XX.......XX +.XX..X..XX. +..XXXXXXX.. +XXXXXXXXXXX +...XXXXX... +....XXX.... +...R.X.G... +..R..X..G.. +.RR.XXX.GG. +""" +MICRO = """ +X.....X +XX.X.XX +.XXXXX. +..XXX.. +...X... +.R.X.G. +R..X..G +""" +Grid = dict[tuple[int, int], str] + + +def _inside(x: float, y: float, kind: str, p: Any) -> bool: + if kind == "circ": + return math.hypot(x - p[0], y - p[1]) <= p[2] + if kind == "cap": + pts, r = p + return min(_seg(x, y, pts[i], pts[i + 1]) for i in range(len(pts) - 1)) <= r + cx, cy, rx, ry = p + return ((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2 <= 1 + + +def detailed() -> tuple[Grid, int, int]: + cols, rows = DETAILED_SIZE + g: Grid = {} + for r in range(rows): + for c in range(cols): + for kind, p, layer in SHAPES: + if _inside(c + 0.5, r + 0.5, kind, p): + g[(c, r)] = layer + break + return g, cols, rows + + +def art(s: str) -> tuple[Grid, int, int]: + lines = s.strip().splitlines() + g = {(c, r): {"X": "gold", "R": "red", "G": "green"}[ch] for r, ln in enumerate(lines) for c, ch in enumerate(ln) + if ch != "."} # fmt: skip + return g, len(lines[0]), len(lines)