|
| 1 | +"""Render agentseam's brand assets: the GitHub social preview card and the mark. |
| 2 | +
|
| 3 | +Run from this directory: |
| 4 | +
|
| 5 | + pip install cairosvg # asset tooling only |
| 6 | + python gen_brand_assets.py |
| 7 | +
|
| 8 | +Writes social-preview.svg/.png (1280x640, GitHub's social-preview size) and |
| 9 | +logo.svg/logo-512.png (512x512) beside this file. |
| 10 | +
|
| 11 | +cairosvg is NOT a dependency of agentseam. The package's runtime path is stdlib-only and |
| 12 | +`dependencies` in pyproject.toml stays empty; this script is design tooling a maintainer |
| 13 | +runs by hand when the artwork changes, and its output is committed. |
| 14 | +
|
| 15 | +The figure states the project's claim, so it has to be true to it. Two invariants, both |
| 16 | +enforced by the gap tables below and asserted at render time: |
| 17 | +
|
| 18 | + * Nothing crosses. agentseam normalizes the *shape* of a vendor's hook dialect; it does |
| 19 | + not reorder anything. Crossing lines would say otherwise. |
| 20 | + * Every pair converges: each gap on the dialect side is wider than the matching gap on |
| 21 | + the envelope side. A pair that spread apart would read as fan-out, the opposite of |
| 22 | + what the layer does. |
| 23 | +
|
| 24 | +Text renders in Geist Mono, matching the sibling project's cards. Where that font is |
| 25 | +absent the renderer substitutes another monospace face; re-render on a machine that has |
| 26 | +it before committing a changed card. |
| 27 | +""" |
| 28 | + |
| 29 | +import cairosvg |
| 30 | + |
| 31 | +NAVY = "#0D1626" |
| 32 | +GOLD = "#D9B45C" |
| 33 | +DIM = "#8A7340" |
| 34 | +COOL = "#9FB0C4" |
| 35 | +FONT = "Geist Mono" |
| 36 | + |
| 37 | +# Card: 12 dialect lines, one per adapter in src/agentseam/adapters/. Irregular gaps |
| 38 | +# (heterogeneous vendor surfaces), but every one exceeds the envelope's 30.5px gap. |
| 39 | +GAPS = [0, 32, 45, 34, 47, 33, 43, 36, 46, 35, 41, 40] |
| 40 | +# Ragged left edge: the dialects do not start from a tidy margin either. |
| 41 | +RAG = [0, 34, 12, 46, 6, 28, 18, 52, 10, 38, 22, 44] |
| 42 | +# The mark carries only five lines, so its minimum gap has to clear a wider 44px envelope. |
| 43 | +LOGO_GAPS = [0, 1.76, 1.00, 1.36, 1.08] |
| 44 | + |
| 45 | + |
| 46 | +def spread(y0, y1, gaps): |
| 47 | + """n points from y0 to y1 inclusive; gaps[1:] are the n-1 intervals between them.""" |
| 48 | + intervals = gaps[1:] |
| 49 | + total = sum(intervals) |
| 50 | + out = [float(y0)] |
| 51 | + acc = 0.0 |
| 52 | + for gap in intervals: |
| 53 | + acc += gap |
| 54 | + out.append(y0 + (y1 - y0) * (acc / total)) |
| 55 | + return out |
| 56 | + |
| 57 | + |
| 58 | +def check_invariants(left, right): |
| 59 | + """Fail loudly rather than ship a figure that contradicts the claim it illustrates.""" |
| 60 | + lg = [left[i + 1] - left[i] for i in range(len(left) - 1)] |
| 61 | + rg = [right[i + 1] - right[i] for i in range(len(right) - 1)] |
| 62 | + assert all(g > 0 for g in lg + rg), "lines cross: agentseam does not reorder" |
| 63 | + assert all(a > b for a, b in zip(lg, rg)), "a pair diverges: the figure must converge" |
| 64 | + |
| 65 | + |
| 66 | +def seam( |
| 67 | + x0, |
| 68 | + x1, |
| 69 | + sx, |
| 70 | + ytop, |
| 71 | + ybot, |
| 72 | + ly0, |
| 73 | + ly1, |
| 74 | + n, |
| 75 | + swl, |
| 76 | + swr, |
| 77 | + node, |
| 78 | + cap=None, |
| 79 | + cap_y=None, |
| 80 | + cap_size=11, |
| 81 | + opl=0.36, |
| 82 | + stub=3.0, |
| 83 | + rag=1.0, |
| 84 | + gaps=None, |
| 85 | +): |
| 86 | + """Uneven dialect lines on the left, a stitched seam at sx, an even envelope right.""" |
| 87 | + ry = spread(ytop, ybot, [1] * n) # ordered, even: the canonical envelope |
| 88 | + ly = spread(ly0, ly1, (gaps or GAPS)[:n]) # uneven, wider: the vendor dialects |
| 89 | + check_invariants(ly, ry) |
| 90 | + parts = [] |
| 91 | + for i in range(n): |
| 92 | + parts.append( |
| 93 | + f'<line x1="{x0 + RAG[i % len(RAG)] * rag:.1f}" y1="{ly[i]:.1f}" ' |
| 94 | + f'x2="{sx:.1f}" y2="{ry[i]:.1f}" stroke="{GOLD}" stroke-width="{swl}" ' |
| 95 | + f'stroke-opacity="{opl}" stroke-linecap="round"/>' |
| 96 | + ) |
| 97 | + for i in range(n): |
| 98 | + parts.append( |
| 99 | + f'<line x1="{sx:.1f}" y1="{ry[i]:.1f}" x2="{x1:.1f}" y2="{ry[i]:.1f}" ' |
| 100 | + f'stroke="{GOLD}" stroke-width="{swr}" stroke-opacity="0.95" stroke-linecap="round"/>' |
| 101 | + ) |
| 102 | + # The seam itself: one continuous join, not a line broken by the stitches. |
| 103 | + parts.append( |
| 104 | + f'<line x1="{sx:.1f}" y1="{ytop - node * stub:.1f}" x2="{sx:.1f}" ' |
| 105 | + f'y2="{ybot + node * stub:.1f}" stroke="{GOLD}" stroke-width="{swr}" stroke-opacity="0.42"/>' |
| 106 | + ) |
| 107 | + # Each crossing is a stitch: a node where the two planes are held together. |
| 108 | + for i in range(n): |
| 109 | + parts.append( |
| 110 | + f'<rect x="{sx - node:.1f}" y="{ry[i] - node:.1f}" width="{node * 2:.1f}" ' |
| 111 | + f'height="{node * 2:.1f}" transform="rotate(45 {sx:.1f} {ry[i]:.1f})" fill="{GOLD}"/>' |
| 112 | + ) |
| 113 | + if cap: |
| 114 | + parts.append( |
| 115 | + f'<text x="{(x0 + x1) / 2:.1f}" y="{cap_y}" font-family="{FONT}" ' |
| 116 | + f'font-size="{cap_size}" letter-spacing="3.6" fill="{DIM}" text-anchor="middle">{cap}</text>' |
| 117 | + ) |
| 118 | + return "\n ".join(parts) |
| 119 | + |
| 120 | + |
| 121 | +def corners(w, h, m=44, length=16, sw=1.4): |
| 122 | + return "\n ".join( |
| 123 | + f'<path d="M {x + dx * length} {y} L {x} {y} L {x} {y + dy * length}" fill="none" ' |
| 124 | + f'stroke="{GOLD}" stroke-opacity="0.30" stroke-width="{sw}"/>' |
| 125 | + for (x, y, dx, dy) in ((m, m, 1, 1), (w - m, m, -1, 1), (m, h - m, 1, -1), (w - m, h - m, -1, -1)) |
| 126 | + ) |
| 127 | + |
| 128 | + |
| 129 | +W, H = 1280, 640 |
| 130 | +S = 512 |
| 131 | +CARD_ALT = ( |
| 132 | + "agentseam — the primitives layer for every coding agent: one handler API over " |
| 133 | + "per-agent hooks, instruction files, plugin packaging, and config. Twelve unevenly " |
| 134 | + "spaced vendor hook dialects converge through a stitched seam into one evenly spaced " |
| 135 | + "canonical envelope." |
| 136 | +) |
| 137 | +LOGO_ALT = ( |
| 138 | + "agentseam logo: five unevenly spaced vendor lines converging through a stitched seam " |
| 139 | + "into five evenly spaced parallel lines." |
| 140 | +) |
| 141 | + |
| 142 | + |
| 143 | +def text(x, y, size, fill, body, weight=None, tracking=None): |
| 144 | + attrs = [f'x="{x}"', f'y="{y}"', f'font-family="{FONT}"', f'font-size="{size}"'] |
| 145 | + if weight: |
| 146 | + attrs.append(f'font-weight="{weight}"') |
| 147 | + if tracking is not None: |
| 148 | + attrs.append(f'letter-spacing="{tracking}"') |
| 149 | + attrs.append(f'fill="{fill}"') |
| 150 | + return "<text " + " ".join(attrs) + f">{body}</text>" |
| 151 | + |
| 152 | + |
| 153 | +def build_card(): |
| 154 | + body = [ |
| 155 | + f'<rect width="{W}" height="{H}" fill="{NAVY}"/>', |
| 156 | + corners(W, H), |
| 157 | + text(88, 236, 13, DIM, "PRIMITIVES LAYER", tracking=5.2), |
| 158 | + text(84, 322, 78, GOLD, "agentseam", weight=700, tracking=-1.5), |
| 159 | + f'<line x1="88" y1="356" x2="566" y2="356" stroke="{GOLD}" stroke-opacity="0.28" stroke-width="1.2"/>', |
| 160 | + text(88, 398, 19, COOL, "One handler API over per-agent hooks,"), |
| 161 | + text(88, 426, 19, COOL, "instruction files, plugin packaging, config."), |
| 162 | + text(88, 524, 12, DIM, "12 ADAPTERS · STDLIB-ONLY · APACHE-2.0", tracking=3.4), |
| 163 | + seam( |
| 164 | + 664, |
| 165 | + 1192, |
| 166 | + 930, |
| 167 | + 152, |
| 168 | + 488, |
| 169 | + 104, |
| 170 | + 536, |
| 171 | + 12, |
| 172 | + 1.6, |
| 173 | + 2.0, |
| 174 | + 4.2, |
| 175 | + cap="VENDOR DIALECTS → CANONICAL ENVELOPE", |
| 176 | + cap_y=576, |
| 177 | + ), |
| 178 | + ] |
| 179 | + return ( |
| 180 | + f'<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" viewBox="0 0 {W} {H}"\n' |
| 181 | + f' role="img" aria-label="{CARD_ALT}">\n ' + "\n ".join(body) + "\n</svg>" |
| 182 | + ) |
| 183 | + |
| 184 | + |
| 185 | +def build_logo(): |
| 186 | + mark = seam( |
| 187 | + 96, |
| 188 | + 416, |
| 189 | + 252, |
| 190 | + 168, |
| 191 | + 344, |
| 192 | + 126, |
| 193 | + 386, |
| 194 | + 5, |
| 195 | + 5.0, |
| 196 | + 6.5, |
| 197 | + 9, |
| 198 | + opl=0.52, |
| 199 | + stub=1.8, |
| 200 | + rag=0.55, |
| 201 | + gaps=LOGO_GAPS, |
| 202 | + ) |
| 203 | + return ( |
| 204 | + f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {S} {S}" role="img"\n' |
| 205 | + f' aria-label="{LOGO_ALT}">\n' |
| 206 | + f' <rect width="{S}" height="{S}" rx="104" fill="{NAVY}"/>\n ' + mark + "\n</svg>" |
| 207 | + ) |
| 208 | + |
| 209 | + |
| 210 | +if __name__ == "__main__": |
| 211 | + card, logo = build_card(), build_logo() |
| 212 | + for name, source in (("social-preview", card), ("logo", logo)): |
| 213 | + with open(f"{name}.svg", "w", encoding="utf-8") as fh: |
| 214 | + fh.write(source) |
| 215 | + cairosvg.svg2png(bytestring=card.encode(), write_to="social-preview.png", output_width=W, output_height=H) |
| 216 | + cairosvg.svg2png(bytestring=logo.encode(), write_to="logo-512.png", output_width=S, output_height=S) |
| 217 | + print("rendered social-preview.svg/.png and logo.svg/logo-512.png") |
0 commit comments