diff --git a/architecture/brand-marks.md b/architecture/brand-marks.md index 42908d7..bfac012 100644 --- a/architecture/brand-marks.md +++ b/architecture/brand-marks.md @@ -20,10 +20,28 @@ dimensions in `render.py`. One large-format logo per repo: the constant green+gold snake-frame (`geometry.py::project_frame`, margin 9 / arm 53 / stroke 11) with a single gold inner symbol (`symbols.py`) chosen per repo in `projects.py::MANIFEST`. -Two-colour (green + gold); repos differ by symbol shape, not colour. The two -project templates reuse the org chevron. `modern-di-faststream` is the only -mark using a partner's literal logo path (FastStream's, recoloured); other -integration cues are redrawn evocations. Outputs: `mark.svg`, +Two-colour (green + gold); repos differ by symbol shape, not colour. Every mark +renders on a transparent background, so `CREAM` is only ever a knockout painted +ON TOP OF a `GOLD` shape, never standalone ink directly on the background: +standalone cream is invisible on light surfaces (cream-on-cream) but shows as +stray bright-white ink once the same `mark.svg` sits on a dark one, e.g. +`lockup-dark.svg`'s README banner half. Enforced for **every** manifest repo, with +no exemptions, by `tests/test_projects.py::test_no_cream_on_transparent`, which +rasters each mark with its cream knockouts stripped and fails on any pixel that +turns transparent instead of staying gold-backed. `_cyl`'s cream rim is inset by +half its stroke width for exactly this reason — centred on the top cap's ellipse, +half the stroke would have fallen outside the cylinder's silhouette. The two +project templates reuse the org chevron. Two marks are built from a partner's +literal logo path, recoloured: `modern-di-faststream` (FastStream's) and +`modern-di-flask` (Flask's horn — the Flask Artwork License permits the logo +"or a modified version" to be used to refer to the project). Other +integration cues are redrawn evocations, and three had nothing to redraw +from: `arq` publishes no logo at all, while `aiogram`'s logo is a lettered +disc and `taskiq`'s a wordmark, so those marks are inventions (a Telegram +plane, a hopper, and taskiq's Q-creature). `modern-di-grpc` deliberately +departs from gRPC's mark: theirs needs the arrow and the diamonds to be +different inks, which the gold/cream palette cannot supply, so only the +arrow survives. Outputs: `mark.svg`, `lockup-light.svg`, `lockup-dark.svg`, `lockup.png` (+ `mark-512/1024.png`). Regenerate via `uv run python -m brand.build.render`. Repos with a live docs site (`projects.py::DOCS_REPOS`, a subset of `MANIFEST`) diff --git a/brand/build/projects.py b/brand/build/projects.py index c9bdb0c..752b969 100644 --- a/brand/build/projects.py +++ b/brand/build/projects.py @@ -19,11 +19,17 @@ "modern-di": lambda: sym.graph(_CX, _CY, R, dashed=True), "that-depends": lambda: sym.graph(_CX, _CY, R, dashed=False), "modern-di-aiohttp": lambda: sym.async_loop(_CX, _CY, R), + "modern-di-aiogram": lambda: sym.plane(_CX, _CY, R), + "modern-di-arq": lambda: sym.hopper(_CX, _CY, R), + "modern-di-celery": lambda: sym.celery_stalk(_CX, _CY, R), "modern-di-fastapi": lambda: sym.bolt_disc(_CX, _CY, R), "modern-di-faststream": lambda: sym.faststream(_CX, _CY, R), + "modern-di-flask": lambda: sym.flask_horn(_CX, _CY, R), + "modern-di-grpc": lambda: sym.rpc_arrow(_CX, _CY, R), "modern-di-litestar": lambda: sym.star_disc(_CX, _CY, R), "modern-di-pytest": lambda: sym.bars(_CX, _CY, R), "modern-di-starlette": lambda: sym.sparkle_cluster(_CX, _CY, R), + "modern-di-taskiq": lambda: sym.task_q(_CX, _CY, R), "modern-di-typer": lambda: sym.terminal(_CX, _CY, R), # templates — reuse the org chevron "fastapi-sqlalchemy-template": lambda: sym.chevron(_CX, _CY, R - 1), @@ -38,6 +44,7 @@ "db-retry": lambda: sym.db_retry(_CX, _CY, R), "eof-fixer": lambda: sym.eof_fixer(_CX, _CY, R), "semvertag": lambda: sym.tag(_CX, _CY, R), + "compose2pod": lambda: sym.pod(_CX, _CY, R), } diff --git a/brand/build/symbols.py b/brand/build/symbols.py index 0a9b95f..c63ae40 100644 --- a/brand/build/symbols.py +++ b/brand/build/symbols.py @@ -23,12 +23,20 @@ def _cyl( ) -> str: """Database cylinder centred on (cx,cy).""" rx = 0.5 * r * w + ry = 0.16 * r + top = cy - h / 2 * r + # The top cap's upper arc IS the cylinder's silhouette, so a rim stroke + # centred on that ellipse would put half its width outside the gold — + # invisible on cream, but white ink on dark. Shrink the rim's radii by half + # the stroke width (plus a hair of margin) so the whole stroke lands inside. + rim_w = 0.8 + inset = rim_w / 2 + 0.1 return ( - f'' - f'' - f'' - f'' + f'' + f'' + f'' + f'' ) @@ -76,6 +84,30 @@ def _sparkle4(cx: float, cy: float, radius: float, color: str, inner: float = 0. return f'' +def _box(cx: float, cy: float, s: float, fill: str = GOLD) -> str: + """Rounded square (a container / job token) centred on (cx,cy).""" + return ( + f'' + ) + + +def _ngon(cx: float, cy: float, rad: float, n: int, start: float, w: float) -> str: + """Regular n-gon outline; `start` is the angle (deg) of the first vertex.""" + pts = [ + ( + cx + rad * math.cos(math.radians(start + i * 360 / n)), + cy + rad * math.sin(math.radians(start + i * 360 / n)), + ) + for i in range(n) + ] + body = " ".join(f"{x:.1f},{y:.1f}" for x, y in pts) + return ( + f'' + ) + + def sparkle_cluster(cx: float, cy: float, r: float) -> str: """Starlette cue: a large four-point sparkle with a small companion (a "little star" — starlette).""" @@ -134,6 +166,26 @@ def async_loop(cx: float, cy: float, r: float) -> str: "h370.45c51.9,0,84.33-56.18,58.38-101.12Z" ) +# Flask's own horn, from Pallets' docs/_static/flask-icon.svg (500x500 space). +# Their Flask Artwork License permits the logo "or a modified version" to be used +# by anyone to refer to the Flask project, so this recolour is sanctioned. +FLASK_PATHS = ( + "M224.446,59.975c-0.056,-4.151 -0.483,-5.543 -2.7,-6.823c-2.104,-1.393 -5.288,-1.421 " + "-8.329,-0.085l-204.674,87.64c-3.042,1.336 -5.913,4.008 -7.448,6.908c-1.535,2.899 " + "-1.705,5.97 -0.511,8.158l17.084,31.384l0.228,0.369c1.847,2.928 6.026,3.696 10.29,1.82" + "l1.251,-0.54c5.344,22.4 14.1,50.429 25.783,70.413l178.294,-79.794c-2.559,-23.14 " + "-9.552,-89.602 -9.268,-119.479l0,0.029Z", + "M238.603,205.776l-171.698,76.838c10.091,19.132 22.542,39.428 37.722,58.986c50.429," + "-25.698 100.887,-51.396 151.316,-77.094c-3.269,-8.471 -6.452,-17.653 -17.34,-58.73Z", + "M497.601,388.846l-12.139,-18.535c-1.819,-2.018 -4.633,-2.786 -7.106,-1.791l-15.578," + "5.999c-1.848,-2.047 -4.52,-2.815 -7.135,-1.791c-5.089,1.99 -10.206,4.008 -15.294,5.998" + "c-1.649,0.625 -2.104,1.847 -1.791,3.439l0.995,4.861c-28.711,3.099 -77.236,1.564 " + "-120.701,-32.577c-19.216,-15.066 -37.239,-36.386 -52.277,-66.206l-144.75,73.768" + "c26.466,29.08 59.697,54.864 100.973,70.385c57.422,21.633 130.593,23.679 222.838,-13.475" + "l0.512,2.616c0.455,2.928 3.98,6.026 8.755,4.15l15.323,-5.97c5.258,-1.99 5.287,-6.026 " + "4.519,-8.641l19.729,-7.704c2.217,-0.853 9.096,-6.169 3.183,-14.526l-0.056,-0Z", +) + def bolt_disc(cx: float, cy: float, r: float) -> str: """FastAPI cue: lightning bolt knocked out of a gold disc.""" @@ -364,3 +416,146 @@ def tag(cx: float, cy: float, r: float) -> str: f'L{cx - 0.75 * r:.1f} {cy:.1f} Z" fill="{GOLD}"/>' f'' ) + + +def plane(cx: float, cy: float, r: float) -> str: + """aiogram cue: Telegram's paper plane. aiogram's own logo is a lettered + disc with no pictorial element, so the honest cue is Telegram itself.""" + hull = ((-0.95, 0.02), (0.95, -0.78), (0.30, 0.86), (0.02, 0.28)) + body = " ".join(f"{cx + dx * r:.1f},{cy + dy * r:.1f}" for dx, dy in hull) + # Crease runs fold-point -> nose (hull[3] -> hull[1]), a chord that meets + # the hull exactly at those two vertices. Use a butt cap, not round, so + # the stroke's own width can't spill past the polygon. + # + # The two ends need different insets. hull[1] (nose) is a sharp, + # non-reflex vertex — a butt cap placed at the vertex itself pokes the + # 0.11*r-wide stroke past both adjoining edges, so it needs a real inset + # (~0.154*r is the minimum for this vertex's angle; 0.20*r keeps margin). + # hull[3] (fold) is the polygon's REFLEX vertex (interior angle ~229deg), + # and the crease direction sits ~23deg clear of the nearest edge, so a + # butt cap placed essentially at the fold vertex already stays inside the + # gold (~0.021*r of margin) — only a token inset is needed there. Insetting + # it as much as the nose end made the crease read as a floating stripe + # instead of a fold running notch-to-nose. + fold, nose = hull[3], hull[1] + ex, ey = nose[0] - fold[0], nose[1] - fold[1] + elen = math.hypot(ex, ey) + ux, uy = ex / elen, ey / elen + inset_fold = 0.05 * r + inset_nose = 0.20 * r + x0, y0 = cx + fold[0] * r + ux * inset_fold, cy + fold[1] * r + uy * inset_fold + x1, y1 = cx + nose[0] * r - ux * inset_nose, cy + nose[1] * r - uy * inset_nose + crease = ( + f'' + ) + return f'{crease}' + + +def hopper(cx: float, cy: float, r: float) -> str: + """arq cue: jobs dropping into a hopper, one emerging — a queue being + drained. arq ships no logo, so this is drawn from what arq is.""" + jobs = "".join( + _box(cx + dx * r, cy - 0.72 * r, r * 0.34) for dx in (-0.52, 0.0, 0.52) + ) + hull = ((-0.86, -0.30), (0.86, -0.30), (0.20, 0.34), (-0.20, 0.34)) + funnel = " ".join(f"{cx + dx * r:.1f},{cy + dy * r:.1f}" for dx, dy in hull) + return ( + f'{jobs}' + + _box(cx, cy + 0.76 * r, r * 0.34) + ) + + +def pod(cx: float, cy: float, r: float) -> str: + """compose2pod cue: Podman's heptagon holding three composed containers — + both ends of the Compose-to-Podman conversion in one shape.""" + return _ngon(cx, cy, r * 0.98, 7, 90, r * 0.15) + ( + _box(cx - 0.34 * r, cy - 0.20 * r, r * 0.36) + + _box(cx + 0.30 * r, cy - 0.20 * r, r * 0.36) + + _box(cx - 0.02 * r, cy + 0.30 * r, r * 0.36) + ) + + +def celery_stalk(cx: float, cy: float, r: float) -> str: + """Celery cue: their icon redrawn — a banded capsule with an open C, + stroked the same gold as the body, at its right end. Measured off + celery_512.png; Celery ships no SVG (see celery/celery#5981), and the C + is *open* (facing right), not a closed ring.""" + s = (2.35 * r) / 512.0 + body = ( + '' + ) + band_w = 9.0 + # The capsule's left cap is a semicircle (centre 103,256 r=102), so its + # boundary curves inward away from y=256. A plain x1="8" pokes past that + # curve for the off-centre bands. Inset each band's start to the cap's + # boundary at the band's *farthest* edge (accounting for the stroke's own + # width), plus a small safety margin, so the whole stroke — not just its + # centreline — stays inside the gold. max() keeps the already-safe centre + # band (whose boundary is well left of 8) visually unchanged. + bands = "".join( + f'' + for y in (205, 256, 307) + ) + arc = ( + '' + ) + return ( + f'{body}{bands}{arc}' + ) + + +def task_q(cx: float, cy: float, r: float) -> str: + """taskiq cue: the Q-creature from their wordmark — a thick ring with a + descender crossing it. taskiq publishes no icon-only mark.""" + ang = math.radians(48) + dx, dy = math.cos(ang), math.sin(ang) + ring_y = cy - 0.06 * r + ring = ( + f'' + ) + tail = ( + f'' + ) + return ring + tail + + +def flask_horn(cx: float, cy: float, r: float) -> str: + """Flask cue: Flask's own drinking horn (a rhyton — not a lab flask), + recoloured gold. The bands are negative space in the source artwork.""" + sc = (2.04 * r) / 500.0 + body = "".join(f'' for d in FLASK_PATHS) + return ( + f'{body}' + ) + + +def rpc_arrow(cx: float, cy: float, r: float) -> str: + """gRPC cue: a bidirectional call. gRPC's real mark (two diamonds pierced by + an arrow) needs two inks that contrast with each other *and* with the page; + our two-colour palette cannot supply that, so the mark keeps the arrow alone, + with the heads symmetrized (gRPC's own are unequal, sized to their diamonds).""" + w = r * 0.17 + half, reach, rise = 0.98 * r, 0.34 * r, 0.46 * r + out = ( + f'' + ) + for sgn in (-1, 1): + tx = cx + sgn * half + out += ( + f'' + ) + return out diff --git a/brand/projects/compose2pod/lockup-dark.svg b/brand/projects/compose2pod/lockup-dark.svg new file mode 100644 index 0000000..f5b1bae --- /dev/null +++ b/brand/projects/compose2pod/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/compose2pod/lockup-light.svg b/brand/projects/compose2pod/lockup-light.svg new file mode 100644 index 0000000..0850ba6 --- /dev/null +++ b/brand/projects/compose2pod/lockup-light.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/compose2pod/lockup.png b/brand/projects/compose2pod/lockup.png new file mode 100644 index 0000000..43b5e2c Binary files /dev/null and b/brand/projects/compose2pod/lockup.png differ diff --git a/brand/projects/compose2pod/mark-1024.png b/brand/projects/compose2pod/mark-1024.png new file mode 100644 index 0000000..90d53fe Binary files /dev/null and b/brand/projects/compose2pod/mark-1024.png differ diff --git a/brand/projects/compose2pod/mark-512.png b/brand/projects/compose2pod/mark-512.png new file mode 100644 index 0000000..2e83db4 Binary files /dev/null and b/brand/projects/compose2pod/mark-512.png differ diff --git a/brand/projects/compose2pod/mark.svg b/brand/projects/compose2pod/mark.svg new file mode 100644 index 0000000..ebe1890 --- /dev/null +++ b/brand/projects/compose2pod/mark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/db-retry/lockup-dark.svg b/brand/projects/db-retry/lockup-dark.svg index be22819..9ae1b9f 100644 --- a/brand/projects/db-retry/lockup-dark.svg +++ b/brand/projects/db-retry/lockup-dark.svg @@ -1 +1 @@ - + diff --git a/brand/projects/db-retry/lockup-light.svg b/brand/projects/db-retry/lockup-light.svg index ad84956..7ad437d 100644 --- a/brand/projects/db-retry/lockup-light.svg +++ b/brand/projects/db-retry/lockup-light.svg @@ -1 +1 @@ - + diff --git a/brand/projects/db-retry/lockup.png b/brand/projects/db-retry/lockup.png index 9b04bac..eda26a3 100644 Binary files a/brand/projects/db-retry/lockup.png and b/brand/projects/db-retry/lockup.png differ diff --git a/brand/projects/db-retry/mark-1024.png b/brand/projects/db-retry/mark-1024.png index 2805038..d5d4e8b 100644 Binary files a/brand/projects/db-retry/mark-1024.png and b/brand/projects/db-retry/mark-1024.png differ diff --git a/brand/projects/db-retry/mark-512.png b/brand/projects/db-retry/mark-512.png index 57bb47c..c7dc8eb 100644 Binary files a/brand/projects/db-retry/mark-512.png and b/brand/projects/db-retry/mark-512.png differ diff --git a/brand/projects/db-retry/mark.svg b/brand/projects/db-retry/mark.svg index d485876..26d9d28 100644 --- a/brand/projects/db-retry/mark.svg +++ b/brand/projects/db-retry/mark.svg @@ -1 +1 @@ - + diff --git a/brand/projects/faststream-outbox/lockup-dark.svg b/brand/projects/faststream-outbox/lockup-dark.svg index bf0970b..7f54e4d 100644 --- a/brand/projects/faststream-outbox/lockup-dark.svg +++ b/brand/projects/faststream-outbox/lockup-dark.svg @@ -1 +1 @@ - + diff --git a/brand/projects/faststream-outbox/lockup-light.svg b/brand/projects/faststream-outbox/lockup-light.svg index a5f9ac9..11a94ce 100644 --- a/brand/projects/faststream-outbox/lockup-light.svg +++ b/brand/projects/faststream-outbox/lockup-light.svg @@ -1 +1 @@ - + diff --git a/brand/projects/faststream-outbox/lockup.png b/brand/projects/faststream-outbox/lockup.png index f3543d0..caf9e7e 100644 Binary files a/brand/projects/faststream-outbox/lockup.png and b/brand/projects/faststream-outbox/lockup.png differ diff --git a/brand/projects/faststream-outbox/mark-1024.png b/brand/projects/faststream-outbox/mark-1024.png index 046f5dc..0a6c3a4 100644 Binary files a/brand/projects/faststream-outbox/mark-1024.png and b/brand/projects/faststream-outbox/mark-1024.png differ diff --git a/brand/projects/faststream-outbox/mark-512.png b/brand/projects/faststream-outbox/mark-512.png index 07e6875..68c6c23 100644 Binary files a/brand/projects/faststream-outbox/mark-512.png and b/brand/projects/faststream-outbox/mark-512.png differ diff --git a/brand/projects/faststream-outbox/mark.svg b/brand/projects/faststream-outbox/mark.svg index 10d3337..1481550 100644 --- a/brand/projects/faststream-outbox/mark.svg +++ b/brand/projects/faststream-outbox/mark.svg @@ -1 +1 @@ - + diff --git a/brand/projects/faststream-outbox/social-card.png b/brand/projects/faststream-outbox/social-card.png index ed87a5d..14145e4 100644 Binary files a/brand/projects/faststream-outbox/social-card.png and b/brand/projects/faststream-outbox/social-card.png differ diff --git a/brand/projects/faststream-outbox/social-card.svg b/brand/projects/faststream-outbox/social-card.svg index 41d3a51..0769625 100644 --- a/brand/projects/faststream-outbox/social-card.svg +++ b/brand/projects/faststream-outbox/social-card.svg @@ -1 +1 @@ - + diff --git a/brand/projects/modern-di-aiogram/lockup-dark.svg b/brand/projects/modern-di-aiogram/lockup-dark.svg new file mode 100644 index 0000000..2286676 --- /dev/null +++ b/brand/projects/modern-di-aiogram/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-aiogram/lockup-light.svg b/brand/projects/modern-di-aiogram/lockup-light.svg new file mode 100644 index 0000000..f3a55e9 --- /dev/null +++ b/brand/projects/modern-di-aiogram/lockup-light.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-aiogram/lockup.png b/brand/projects/modern-di-aiogram/lockup.png new file mode 100644 index 0000000..bb07cbb Binary files /dev/null and b/brand/projects/modern-di-aiogram/lockup.png differ diff --git a/brand/projects/modern-di-aiogram/mark-1024.png b/brand/projects/modern-di-aiogram/mark-1024.png new file mode 100644 index 0000000..9f7e5df Binary files /dev/null and b/brand/projects/modern-di-aiogram/mark-1024.png differ diff --git a/brand/projects/modern-di-aiogram/mark-512.png b/brand/projects/modern-di-aiogram/mark-512.png new file mode 100644 index 0000000..60bba14 Binary files /dev/null and b/brand/projects/modern-di-aiogram/mark-512.png differ diff --git a/brand/projects/modern-di-aiogram/mark.svg b/brand/projects/modern-di-aiogram/mark.svg new file mode 100644 index 0000000..b929cac --- /dev/null +++ b/brand/projects/modern-di-aiogram/mark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-arq/lockup-dark.svg b/brand/projects/modern-di-arq/lockup-dark.svg new file mode 100644 index 0000000..ed076c9 --- /dev/null +++ b/brand/projects/modern-di-arq/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-arq/lockup-light.svg b/brand/projects/modern-di-arq/lockup-light.svg new file mode 100644 index 0000000..7589a9a --- /dev/null +++ b/brand/projects/modern-di-arq/lockup-light.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-arq/lockup.png b/brand/projects/modern-di-arq/lockup.png new file mode 100644 index 0000000..695098f Binary files /dev/null and b/brand/projects/modern-di-arq/lockup.png differ diff --git a/brand/projects/modern-di-arq/mark-1024.png b/brand/projects/modern-di-arq/mark-1024.png new file mode 100644 index 0000000..b18edc9 Binary files /dev/null and b/brand/projects/modern-di-arq/mark-1024.png differ diff --git a/brand/projects/modern-di-arq/mark-512.png b/brand/projects/modern-di-arq/mark-512.png new file mode 100644 index 0000000..bdc64f8 Binary files /dev/null and b/brand/projects/modern-di-arq/mark-512.png differ diff --git a/brand/projects/modern-di-arq/mark.svg b/brand/projects/modern-di-arq/mark.svg new file mode 100644 index 0000000..1ab4d13 --- /dev/null +++ b/brand/projects/modern-di-arq/mark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-celery/lockup-dark.svg b/brand/projects/modern-di-celery/lockup-dark.svg new file mode 100644 index 0000000..275dcea --- /dev/null +++ b/brand/projects/modern-di-celery/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-celery/lockup-light.svg b/brand/projects/modern-di-celery/lockup-light.svg new file mode 100644 index 0000000..558743a --- /dev/null +++ b/brand/projects/modern-di-celery/lockup-light.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-celery/lockup.png b/brand/projects/modern-di-celery/lockup.png new file mode 100644 index 0000000..2dc93cb Binary files /dev/null and b/brand/projects/modern-di-celery/lockup.png differ diff --git a/brand/projects/modern-di-celery/mark-1024.png b/brand/projects/modern-di-celery/mark-1024.png new file mode 100644 index 0000000..438161e Binary files /dev/null and b/brand/projects/modern-di-celery/mark-1024.png differ diff --git a/brand/projects/modern-di-celery/mark-512.png b/brand/projects/modern-di-celery/mark-512.png new file mode 100644 index 0000000..0859af7 Binary files /dev/null and b/brand/projects/modern-di-celery/mark-512.png differ diff --git a/brand/projects/modern-di-celery/mark.svg b/brand/projects/modern-di-celery/mark.svg new file mode 100644 index 0000000..0582ebf --- /dev/null +++ b/brand/projects/modern-di-celery/mark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-flask/lockup-dark.svg b/brand/projects/modern-di-flask/lockup-dark.svg new file mode 100644 index 0000000..946c5cb --- /dev/null +++ b/brand/projects/modern-di-flask/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-flask/lockup-light.svg b/brand/projects/modern-di-flask/lockup-light.svg new file mode 100644 index 0000000..3805246 --- /dev/null +++ b/brand/projects/modern-di-flask/lockup-light.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-flask/lockup.png b/brand/projects/modern-di-flask/lockup.png new file mode 100644 index 0000000..3582433 Binary files /dev/null and b/brand/projects/modern-di-flask/lockup.png differ diff --git a/brand/projects/modern-di-flask/mark-1024.png b/brand/projects/modern-di-flask/mark-1024.png new file mode 100644 index 0000000..f062d59 Binary files /dev/null and b/brand/projects/modern-di-flask/mark-1024.png differ diff --git a/brand/projects/modern-di-flask/mark-512.png b/brand/projects/modern-di-flask/mark-512.png new file mode 100644 index 0000000..fd191dc Binary files /dev/null and b/brand/projects/modern-di-flask/mark-512.png differ diff --git a/brand/projects/modern-di-flask/mark.svg b/brand/projects/modern-di-flask/mark.svg new file mode 100644 index 0000000..48dbd54 --- /dev/null +++ b/brand/projects/modern-di-flask/mark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-grpc/lockup-dark.svg b/brand/projects/modern-di-grpc/lockup-dark.svg new file mode 100644 index 0000000..f853f1b --- /dev/null +++ b/brand/projects/modern-di-grpc/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-grpc/lockup-light.svg b/brand/projects/modern-di-grpc/lockup-light.svg new file mode 100644 index 0000000..65b5c4c --- /dev/null +++ b/brand/projects/modern-di-grpc/lockup-light.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-grpc/lockup.png b/brand/projects/modern-di-grpc/lockup.png new file mode 100644 index 0000000..8d6f3b1 Binary files /dev/null and b/brand/projects/modern-di-grpc/lockup.png differ diff --git a/brand/projects/modern-di-grpc/mark-1024.png b/brand/projects/modern-di-grpc/mark-1024.png new file mode 100644 index 0000000..3397b71 Binary files /dev/null and b/brand/projects/modern-di-grpc/mark-1024.png differ diff --git a/brand/projects/modern-di-grpc/mark-512.png b/brand/projects/modern-di-grpc/mark-512.png new file mode 100644 index 0000000..50e2b5b Binary files /dev/null and b/brand/projects/modern-di-grpc/mark-512.png differ diff --git a/brand/projects/modern-di-grpc/mark.svg b/brand/projects/modern-di-grpc/mark.svg new file mode 100644 index 0000000..a710c11 --- /dev/null +++ b/brand/projects/modern-di-grpc/mark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-taskiq/lockup-dark.svg b/brand/projects/modern-di-taskiq/lockup-dark.svg new file mode 100644 index 0000000..d2e29f9 --- /dev/null +++ b/brand/projects/modern-di-taskiq/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-taskiq/lockup-light.svg b/brand/projects/modern-di-taskiq/lockup-light.svg new file mode 100644 index 0000000..c768627 --- /dev/null +++ b/brand/projects/modern-di-taskiq/lockup-light.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-taskiq/lockup.png b/brand/projects/modern-di-taskiq/lockup.png new file mode 100644 index 0000000..72f1c50 Binary files /dev/null and b/brand/projects/modern-di-taskiq/lockup.png differ diff --git a/brand/projects/modern-di-taskiq/mark-1024.png b/brand/projects/modern-di-taskiq/mark-1024.png new file mode 100644 index 0000000..09520ec Binary files /dev/null and b/brand/projects/modern-di-taskiq/mark-1024.png differ diff --git a/brand/projects/modern-di-taskiq/mark-512.png b/brand/projects/modern-di-taskiq/mark-512.png new file mode 100644 index 0000000..93d453d Binary files /dev/null and b/brand/projects/modern-di-taskiq/mark-512.png differ diff --git a/brand/projects/modern-di-taskiq/mark.svg b/brand/projects/modern-di-taskiq/mark.svg new file mode 100644 index 0000000..1247168 --- /dev/null +++ b/brand/projects/modern-di-taskiq/mark.svg @@ -0,0 +1 @@ + diff --git a/planning/changes/2026-07-12.01-seven-new-project-marks.md b/planning/changes/2026-07-12.01-seven-new-project-marks.md new file mode 100644 index 0000000..1a2200d --- /dev/null +++ b/planning/changes/2026-07-12.01-seven-new-project-marks.md @@ -0,0 +1,150 @@ +--- +summary: Mint brand marks for the seven repos that have none — six modern-di integrations (aiogram, arq, celery, flask, grpc, taskiq) and compose2pod — adding their symbols, MANIFEST entries and tests, and regenerating the brand kit. +--- + +# Design: Brand marks for the seven un-branded repos + +## Summary + +Seven repos in the org have no entry in the brand kit: `modern-di-aiogram`, +`modern-di-arq`, `modern-di-celery`, `modern-di-flask`, `modern-di-grpc`, +`modern-di-taskiq`, and `compose2pod`. This change mints a symbol for each, +registers them in `projects.py::MANIFEST`, extends the test set, and regenerates +`brand/projects//`. Scope is the brand kit only — nothing in +`profile/README.md`, `docs/`, or GitHub repo settings. + +## Motivation + +Every other repo in the org has a mark, a lockup pair, and a PNG fallback under +`brand/projects//`; these seven have nothing, so they cannot carry the +README `` banner their siblings all use. The gap is the whole reason +for the change — there is no broken link to fix yet (unlike the +`modern-di-starlette` onboarding, whose README already pointed at absent files), +but the seven repos cannot adopt the banner convention until the assets exist. + +Each mark was chosen against the partner's **actual** logo, verified by fetching +the upstream asset rather than working from memory. That turned up three facts +that shaped the design: + +- **arq ships no logo at all** — its README is badges only. Its mark is + necessarily an invention from what arq *is*, not a quotation. +- **aiogram's logo is a lettered blue disc** with no pictorial element, and + **taskiq's is a wordmark** whose only glyph is a tailed Q. Neither offers a + symbol to borrow, so both marks are evocations. +- **Flask's logo is a drinking horn (rhyton), not a lab flask** — and Flask's + Artwork License states the logo "or a modified version may be used by anyone + to refer to the Flask project". That permission makes a literal recolour the + correct choice, and it removes the honesty-versus-legibility tension that a + hand-drawn horn could not resolve. + +## Design + +### 1. Seven symbols — `brand/build/symbols.py` + +Each follows the existing contract: `(cx, cy, r) -> str`, bare markup, `GOLD` +fills with `CREAM` knockouts, so `test_only_allowed_colours` passes unchanged. + +| repo | symbol | cue | +| --- | --- | --- | +| `modern-di-aiogram` | `plane` | Telegram's paper plane (aiogram itself has no pictorial mark) | +| `modern-di-arq` | `hopper` | jobs falling into a hopper, one emerging — a queue being drained | +| `modern-di-celery` | `celery_stalk` | Celery's icon redrawn: banded capsule + right-facing open C | +| `modern-di-flask` | `flask_horn` | Flask's literal `flask-icon.svg` path, recoloured | +| `modern-di-grpc` | `rpc_arrow` | a symmetric bidirectional arrow | +| `modern-di-taskiq` | `task_q` | taskiq's Q-creature: thick ring, crossing descender | +| `compose2pod` | `pod` | Podman's heptagon holding three composed containers | + +Two of these need explanation. + +**`flask_horn` embeds a partner's literal path.** The three paths from Pallets' +`flask-icon.svg` become a module constant beside the existing `FASTSTREAM_PATH`, +scaled and recoloured gold. The icon is a single flat fill whose bands are pure +negative space, so a recolour needs no reconstruction. This makes Flask the +*second* mark built from a partner's real artwork, which contradicts a sentence +in `architecture/brand-marks.md` — see §4. + +**`rpc_arrow` is redrawn, not traced.** gRPC publishes no icon-only asset (every +file they and the CNCF label "icon" contains the wordmark), and their true mark — +two diamonds pierced by an arrow — cannot survive our two-colour palette: the +arrow is a *different colour* from the diamonds upstream (dark slate on teal), +but our inner symbols only get gold and cream, and cream fails in the gap +between the diamonds where the background is already cream. Every faithful +rendering turned to mush. The mark therefore keeps only the arrow, symmetrized +(gRPC's own heads are deliberately unequal, sized to their unequal diamonds; that +asymmetry reads as a defect once the diamonds are gone). The accepted cost: the +mark is no longer a quotation of gRPC's logo, just a generic "calls both ways". + +### 2. Manifest — `brand/build/projects.py` + +Seven `MANIFEST` entries. The six `modern-di-*` integrations join the +dependency-injection block; `compose2pod` joins the utilities block. + +### 3. Tests — `tests/test_projects.py` + +Add all seven to `EXPECTED_REPOS`. That set is asserted equal to `MANIFEST`, and +the valid-SVG, allowed-colours, and render tests are parametrized off it, so no +other test edits are required. `DOCS_EXPECTED` is unchanged. + +### 4. Architecture — `architecture/brand-marks.md` + +The per-project section states that `modern-di-faststream` "is the only mark +using a partner's literal logo path". Flask's horn makes that false; the sentence +is corrected to name both, and to record that Pallets' Artwork License permits +the derivative. + +### 5. Regenerate — `just sync-assets` + +Produces `mark.svg`, `mark-512.png`, `mark-1024.png`, `lockup-light.svg`, +`lockup-dark.svg`, and `lockup.png` for each of the seven. Generated files are +committed. + +## Non-goals + +- **No social cards.** None of the seven ships an `mkdocs.yml`, so none has a + docs site and none belongs in `DOCS_REPOS`. +- **No profile or docs-site listings**, and no GitHub description/topics/website + changes. Those surfaces are a separate change. +- **No edits to the seven repos themselves** — the README `` banners + hotlink these assets and can be added per-repo afterwards. + +## Testing + +- `just test` green — in particular `test_manifest_covers_every_repo` + (`MANIFEST == EXPECTED_REPOS`), `test_only_allowed_colours`, + `test_project_mark_is_valid_svg`, and the render tests, all now covering 26 + repos. +- `just check-planning` passes before pushing. +- Visually inspect the regenerated `mark.svg` and both lockups for each of the + seven, and check the seven against the 19 existing marks as a set. + +## Risk + +- **Low: a mark reads as something unintended.** `compose2pod`'s three containers + inside the heptagon can read as a face. Accepted knowingly; the shape encodes + both ends of the Compose-to-Podman conversion and no cleaner alternative did. +- **Low: Celery's proportions.** Its real icon is a wide, short bar (~2.5:1), so + it fills the square frame less fully than its siblings. Acceptable — per-project + marks are large-format logos (README banners, lockups); only the *org* mark is + used at favicon size. +- **Low: Flask's artwork terms drift.** The Artwork License granting derivative + use was dropped from the Pallets repo after Flask 1.0 and nothing replaced it, + so today's assets ship with no stated terms. The grant we rely on is the + historical one, and it was explicit. If Pallets later publishes restrictive + terms, the horn is one function to redraw. +- **Low: stray colour fails the palette test.** All seven use only `GOLD` and + `CREAM`; `test_only_allowed_colours` is the guardrail. +- **Medium (realized, fixed): CREAM must only knock out of GOLD, never sit + directly on the transparent background.** Marks render on dark surfaces too + (`lockup-dark.svg` is the dark half of every README `` banner), so + standalone cream ink — invisible on a cream/light page — becomes bright + white ink on dark. Three of the seven symbols shipped this bug (`task_q`'s + eye sat in the ring's hollow counter with no gold beneath it; `celery_stalk`'s + bands poked past the capsule's curved cap; `plane`'s round-capped crease + spilled past the hull at the nose) and were fixed in a follow-up pass, with + `test_no_cream_on_transparent` added as the regression guard. Auditing the + guard across the whole manifest then exposed two *pre-existing* marks with the + same defect — `db-retry` and `faststream-outbox`, whose shared `_cyl` helper + stroked its cream rim centred on the top cap's ellipse, putting half the stroke + outside the cylinder's silhouette. `_cyl`'s rim is now inset by half its stroke + width, so the guard covers every mark with no exemptions. The durable lesson: + composite every mark onto a dark background before it ships, not just a light one. diff --git a/tests/test_projects.py b/tests/test_projects.py index c93ee6c..d618258 100644 --- a/tests/test_projects.py +++ b/tests/test_projects.py @@ -1,10 +1,13 @@ import re +import shutil from pathlib import Path from xml.dom import minidom import pytest +from PIL import Image from brand.build import geometry as g from brand.build import tokens as t from brand.build import projects as p +from brand.build.raster import export_png def test_project_frame_parses_and_uses_tokens() -> None: @@ -25,6 +28,7 @@ def test_project_frame_parses_and_uses_tokens() -> None: "modern-di-faststream", "modern-di-typer", "modern-di-pytest", + "modern-di-aiogram", "fastapi-sqlalchemy-template", "litestar-sqlalchemy-template", "lite-bootstrap", @@ -35,6 +39,12 @@ def test_project_frame_parses_and_uses_tokens() -> None: "db-retry", "eof-fixer", "semvertag", + "modern-di-arq", + "compose2pod", + "modern-di-celery", + "modern-di-taskiq", + "modern-di-flask", + "modern-di-grpc", } @@ -153,3 +163,74 @@ def test_render_projects_writes_cards_for_docs_repos_only(tmp_path: Path) -> Non non_docs = set(p.MANIFEST) - DOCS_EXPECTED for repo in non_docs: assert not (tmp_path / repo / "social-card.svg").exists() + + +# Regression guard for the transparent-background invariant: every mark is +# drawn on a transparent background, and CREAM is only ever a knockout +# painted ON TOP OF a GOLD shape — never standalone ink directly on the +# background. Violate that and the mark looks fine on a cream/light page +# (cream-on-cream is invisible) but shows stray bright-white ink once the +# same mark.svg is placed on a dark surface (e.g. lockup-dark.svg's README +# banner half). Parametrized over EXPECTED_REPOS (the full manifest, like +# every other guard in this file) so a repo added later is covered +# automatically instead of needing to be opted in. +# +def _cream_pixels_without_gold_beneath( + repo: str, tmp_path: Path, size: int = 256 +) -> list[tuple[int, int]]: + """Rasterize `repo`'s mark twice — once normally, once with every CREAM + knockout stripped to `none` — and return the (x, y) of every pixel that + is CREAM in the normal render but transparent once CREAM is stripped. + Such a pixel has no gold underneath it: it is standalone cream ink + sitting directly on the transparent background.""" + svg = p.project_mark(repo) + normal_svg = tmp_path / f"{repo}-normal.svg" + normal_svg.write_text(svg, encoding="utf-8") + normal_png = tmp_path / f"{repo}-normal.png" + assert export_png(normal_svg, normal_png, width=size, height=size) + + stripped = svg.replace(t.CREAM, "none") + # A literal, case-sensitive replace silently no-ops if cream were ever + # written in a different form (e.g. uppercase hex, rgb()) — make that + # loud instead of letting the guard pass vacuously against an unchanged + # (still-opaque) render. + if t.CREAM in svg: + assert stripped != svg, ( + f"{repo}: stripping {t.CREAM} left the SVG unchanged — " + "the guard would compare a render against itself" + ) + stripped_svg = tmp_path / f"{repo}-stripped.svg" + stripped_svg.write_text(stripped, encoding="utf-8") + stripped_png = tmp_path / f"{repo}-stripped.png" + assert export_png(stripped_svg, stripped_png, width=size, height=size) + + normal_im = Image.open(normal_png).convert("RGBA") + stripped_im = Image.open(stripped_png).convert("RGBA") + cream_rgb = tuple(int(t.CREAM[i : i + 2], 16) for i in (1, 3, 5)) + tol = 10 + bad: list[tuple[int, int]] = [] + for idx, ((r, gg, b, a), (_, _, _, sa)) in enumerate( + zip(normal_im.getdata(), stripped_im.getdata()) + ): + if a < 200: + continue + if ( + abs(r - cream_rgb[0]) < tol + and abs(gg - cream_rgb[1]) < tol + and abs(b - cream_rgb[2]) < tol + and sa < 50 + ): + bad.append((idx % size, idx // size)) + return bad + + +@pytest.mark.skipif( + shutil.which("rsvg-convert") is None, reason="rsvg-convert not installed" +) +@pytest.mark.parametrize("repo", sorted(EXPECTED_REPOS)) +def test_no_cream_on_transparent(repo: str, tmp_path: Path) -> None: + bad = _cream_pixels_without_gold_beneath(repo, tmp_path) + assert not bad, ( + f"{repo}: {len(bad)} cream pixel(s) with no gold beneath, e.g. {bad[:5]} " + "— this mark will show stray white ink on dark surfaces" + ) diff --git a/tests/test_symbols.py b/tests/test_symbols.py index 1f228ae..2f3dd2d 100644 --- a/tests/test_symbols.py +++ b/tests/test_symbols.py @@ -49,3 +49,24 @@ def test_msg_symbol_parses(name: str) -> None: @pytest.mark.parametrize("name", UTIL_SYMBOLS) def test_util_symbol_parses(name: str) -> None: minidom.parseString(_wrap(getattr(sym, name)(50, 50, 23))) + + +NEW_SYMBOLS = [ + "plane", + "hopper", + "pod", + "celery_stalk", + "task_q", + "flask_horn", + "rpc_arrow", +] + + +@pytest.mark.parametrize("name", NEW_SYMBOLS) +def test_new_symbol_parses(name: str) -> None: + minidom.parseString(_wrap(getattr(sym, name)(50, 50, 23))) + + +def test_box_and_ngon_emit_parseable_svg() -> None: + minidom.parseString(_wrap(sym._box(50, 50, 20))) + minidom.parseString(_wrap(sym._ngon(50, 50, 22, 7, 90, 3.5)))