tools/make_brand.py
Rama main · réplica de lectura del repositorio original.
"""Generate every CLIque brand asset from one definition of the mark.
The mark is two chevrons — a large one with a smaller one tucked into its
opening. A prompt, and then a second prompt: many CLIs, one place. The geometry
below was traced from the original drawing and then regularised, so the shapes
are symmetric and the proportions are exact rather than approximately right.
Everything downstream comes from `CHEVRONS`: the SVGs are written from it, and
the PNGs are drawn from the same numbers rather than rasterised from the SVGs.
That means there is no renderer dependency (no Inkscape, no rsvg) and no way
for the vector and the raster to drift apart.
Usage: python3 tools/make_brand.py
"""
from __future__ import annotations
import math
import struct
from pathlib import Path
import numpy as np
from PIL import Image, ImageDraw, ImageFont
OUT = Path(__file__).resolve().parents[1] / "clique" / "web" / "brand"
# --------------------------------------------------------------------- palette
#: Near-black with a trace of blue, so it sits with the panel rather than
#: punching a pure-black hole in it.
INK = "#0E1116"
#: Violet to cyan, running along the chevrons. Chosen because developer tooling
#: is almost uniformly blue — this reads as itself at a glance in a crowded tab
#: strip, and both ends stay legible on light and on dark.
GRAD_FROM = "#A855F7"
GRAD_TO = "#22D3EE"
#: The single colour to use where a gradient cannot go (a 16px favicon glyph,
#: a one-colour print). Sits between the two stops.
SOLID = "#5FA8F5"
#: Corner radius of the squircle, in the 100-unit space.
RADIUS = 22.0
# -------------------------------------------------------------------- geometry
#: (apex_x, apex_y, arm_end_x, arm_half_height, stroke_width) in a 100x100 box.
#: The small chevron's tip stops well short of the large one's inner edge; the
#: gap is part of the mark, not slack.
CHEVRONS = [
(72.0, 50.0, 36.5, 26.8, 10.0),
(36.5, 61.9, 23.3, 11.15, 6.9),
]
#: Corners are softened by stroking the outline in its own fill. One number
#: rounds every corner of both chevrons, which is what keeps them looking like
#: one drawing rather than two.
ROUND = 0.26
def _unit(p, q):
dx, dy = q[0] - p[0], q[1] - p[1]
length = math.hypot(dx, dy)
return dx / length, dy / length
def _offset(p, q, dist):
"""The line p->q shifted `dist` along its right normal."""
ux, uy = _unit(p, q)
nx, ny = uy, -ux
return (p[0] + nx * dist, p[1] + ny * dist), (ux, uy)
def _cross(p1, d1, p2, d2):
den = d1[0] * d2[1] - d1[1] * d2[0]
t = ((p2[0] - p1[0]) * d2[1] - (p2[1] - p1[1]) * d2[0]) / den
return (p1[0] + t * d1[0], p1[1] + t * d1[1])
def _y_at(p, d, x):
return p[1] + (x - p[0]) / d[0] * d[1]
def chevron(apex_x, apex_y, end_x, half_h, width):
"""One chevron as a closed polygon, ends cut vertically.
Built by offsetting each arm's centreline to both sides and intersecting
the offsets — which is what puts the outer tip further right than the
centreline apex and the inner notch further left, exactly as a real stroke
behaves. Drawing it as two overlapping quadrilaterals instead leaves a
visible seam at the join at large sizes.
"""
half = (width - width * ROUND) / 2
upper, apex, lower = (end_x, apex_y - half_h), (apex_x, apex_y), (end_x, apex_y + half_h)
up_out, up_dir = _offset(upper, apex, -half)
up_in, _ = _offset(upper, apex, half)
lo_out, lo_dir = _offset(apex, lower, half)
lo_in, _ = _offset(apex, lower, -half)
return [
(end_x, _y_at(up_out, up_dir, end_x)),
_cross(up_out, up_dir, lo_out, lo_dir),
(end_x, _y_at(lo_out, lo_dir, end_x)),
(end_x, _y_at(lo_in, lo_dir, end_x)),
_cross(up_in, up_dir, lo_in, lo_dir),
(end_x, _y_at(up_in, up_dir, end_x)),
]
def polygons(scale=1.0, dx=0.0, dy=0.0, only=None, bold=1.0):
"""The chevrons as polygons.
`only=1` keeps just the large one, which is all that survives at favicon
size. `bold` thickens the stroke — the mark is drawn at its true weight
everywhere except the very small sizes, where a 2px stroke reads as a wire
outline rather than as a shape.
"""
out = []
for apex_x, apex_y, end_x, half_h, width in CHEVRONS[:only]:
width *= bold
pts = chevron(apex_x, apex_y, end_x, half_h, width)
out.append(([(x * scale + dx, y * scale + dy) for x, y in pts], width * scale * ROUND))
return out
# ------------------------------------------------------------------------ SVG
def _path(points):
head = f"M{points[0][0]:.2f} {points[0][1]:.2f}"
rest = "".join(f"L{x:.2f} {y:.2f}" for x, y in points[1:])
return head + rest + "Z"
def svg_mark(*, square: bool, gradient: bool, size: int = 100) -> str:
"""The mark. `square` adds the tile behind it; `gradient` colours it."""
fill = "url(#cq)" if gradient else "currentColor"
defs = ""
if gradient:
defs = (
"\n <defs>\n"
f' <linearGradient id="cq" gradientUnits="userSpaceOnUse"'
f' x1="{DIAGONAL[0] * 100:.1f}" y1="{DIAGONAL[0] * 100:.1f}"'
f' x2="{DIAGONAL[1] * 100:.1f}" y2="{DIAGONAL[1] * 100:.1f}">\n'
f' <stop offset="0" stop-color="{GRAD_FROM}"/>\n'
f' <stop offset="1" stop-color="{GRAD_TO}"/>\n'
" </linearGradient>\n"
" </defs>"
)
tile = f'\n <rect width="100" height="100" rx="{RADIUS}" fill="{INK}"/>' if square else ""
shapes = "".join(
f'\n <path d="{_path(pts)}" fill="{fill}" stroke="{fill}" '
f'stroke-width="{round_w:.2f}" stroke-linejoin="round"/>'
for pts, round_w in polygons()
)
return (
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" '
f'width="{size}" height="{size}" role="img" aria-label="CLIque">'
f"{defs}{tile}{shapes}\n</svg>\n"
)
def svg_lockup() -> str:
"""Mark plus wordmark, for a README header. Text is a system stack, not a
font file: a lockup that needs a download is a lockup that renders wrong."""
mark = "".join(
f'\n <path d="{_path(pts)}" fill="url(#cq)" stroke="url(#cq)" '
f'stroke-width="{round_w:.2f}" stroke-linejoin="round"/>'
for pts, round_w in polygons()
)
return f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 420 100" width="420" height="100" role="img" aria-label="CLIque: a folder for every CLI on the box">
<defs>
<linearGradient id="cq" gradientUnits="userSpaceOnUse" x1="{DIAGONAL[0] * 100:.1f}" y1="{DIAGONAL[0] * 100:.1f}" x2="{DIAGONAL[1] * 100:.1f}" y2="{DIAGONAL[1] * 100:.1f}">
<stop offset="0" stop-color="{GRAD_FROM}"/>
<stop offset="1" stop-color="{GRAD_TO}"/>
</linearGradient>
</defs>
<g>
<rect width="100" height="100" rx="{RADIUS}" fill="{INK}"/>{mark}
</g>
<text x="122" y="52" font-family="ui-sans-serif,-apple-system,'Segoe UI',Inter,system-ui,sans-serif"
font-size="38" font-weight="700" letter-spacing="-0.5" fill="currentColor">CLIque</text>
<text x="123" y="76" font-family="ui-sans-serif,-apple-system,'Segoe UI',Inter,system-ui,sans-serif"
font-size="15" fill="currentColor" opacity="0.62">Your private clique of CLIs</text>
</svg>
'''
# --------------------------------------------------------------------- raster
SS = 8 # supersampling factor; the mark has long shallow diagonals
def _rgb(value: str):
return tuple(int(value[i : i + 2], 16) for i in (1, 3, 5))
def _diagonal_span():
"""How far along the leading diagonal the mark actually reaches.
Measured from the geometry, not from the tile. The mark never comes near
the tile's corners, so a gradient ramped corner-to-corner paints the whole
drawing out of the middle third of the ramp and renders as one flat blue —
which is exactly what the first attempt did. Deriving it here also means
the colours stay correct if the geometry is ever adjusted.
"""
us = [(x + y) / 200 for pts, _ in polygons() for x, y in pts]
return min(us), max(us)
DIAGONAL = _diagonal_span()
def _gradient(size: int):
"""Violet to cyan along the leading diagonal, as an RGB array."""
axis = np.linspace(0, 1, size)
ramp = np.add.outer(axis, axis) / 2
lo, hi = DIAGONAL
ramp = np.clip((ramp - lo) / (hi - lo), 0, 1)
a, b = np.array(_rgb(GRAD_FROM), float), np.array(_rgb(GRAD_TO), float)
return (a + (b - a) * ramp[..., None]).astype(np.uint8)
def _mark_alpha(size: int, scale: float, dx: float, dy: float, only=None, bold=1.0):
"""Coverage mask for the chevrons, anti-aliased by supersampling."""
big = Image.new("L", (size * SS, size * SS), 0)
pen = ImageDraw.Draw(big)
for pts, round_w in polygons(scale=scale * SS, dx=dx * SS, dy=dy * SS,
only=only, bold=bold):
pen.polygon(pts, fill=255)
# Round the corners the same way the SVG does: stroke the outline in
# its own colour with a joined pen.
pen.line([*pts, pts[0]], fill=255, width=max(1, int(round_w)), joint="curve")
for x, y in pts:
r = round_w / 2
pen.ellipse([x - r, y - r, x + r, y + r], fill=255)
return big.resize((size, size), Image.LANCZOS)
def _squircle_alpha(size: int, radius: float):
big = Image.new("L", (size * SS, size * SS), 0)
ImageDraw.Draw(big).rounded_rectangle(
[0, 0, size * SS - 1, size * SS - 1], radius=radius * SS, fill=255
)
return big.resize((size, size), Image.LANCZOS)
def icon_png(size: int, *, tile: bool = True, inset: float = 0.0) -> Image.Image:
"""One square icon. `inset` shrinks the artwork inside the canvas, which is
what Android's maskable icons need — they crop to a circle on some
launchers and to a squircle on others, so the mark has to live well inside
the safe area rather than at the edge of the tile."""
canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
scale = size / 100.0
if tile:
keep = 1.0 - inset * 2
tile_px = size
radius = RADIUS
if inset:
tile_px = int(size * keep)
art = Image.new("RGBA", (tile_px, tile_px), (0, 0, 0, 0))
art.paste(
Image.new("RGBA", (tile_px, tile_px), (*_rgb(INK), 255)),
(0, 0),
_squircle_alpha(tile_px, radius),
)
grad = Image.fromarray(_gradient(tile_px)).convert("RGBA")
art.paste(grad, (0, 0), _mark_alpha(tile_px, tile_px / 100.0, 0, 0))
off = (size - tile_px) // 2
canvas.paste(art, (off, off), art)
else:
grad = Image.fromarray(_gradient(size)).convert("RGBA")
canvas.paste(grad, (0, 0), _mark_alpha(size, scale, 0, 0))
return canvas
def small_icon_png(size: int, pad: float = 0.06) -> Image.Image:
"""The favicon treatment: the whole mark, no tile, filling the canvas.
Dropping the tile is what buys the room — at 16px its rounding eats the
corners and the mark shrinks to nothing inside it. Transparent also sits
on any tab colour, light or dark, which a fixed dark tile does not.
Framed by measuring the geometry rather than by hand-tuned offsets, which
is how the first version ended up with its tip cropped off the edge.
"""
#: A third heavier, because at 16px the true weight is barely two pixels
#: and reads as a wire outline. An earlier version also dropped the small
#: chevron for legibility, and the result was a tab icon that did not match
#: the one in the window — worse than a slightly busy 16px.
bold = 1.35
pts, round_w = polygons(only=1, bold=bold)[0]
half = round_w / 2
xs = [x for x, _ in pts]
ys = [y for _, y in pts]
x0, x1 = min(xs) - half, max(xs) + half
y0, y1 = min(ys) - half, max(ys) + half
inner = size * (1 - pad * 2)
zoom = inner / max(x1 - x0, y1 - y0)
dx = (size - (x1 - x0) * zoom) / 2 - x0 * zoom
dy = (size - (y1 - y0) * zoom) / 2 - y0 * zoom
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
grad = Image.fromarray(_gradient(size)).convert("RGBA")
img.paste(grad, (0, 0), _mark_alpha(size, zoom, dx, dy, only=1, bold=bold))
return img
def maskable_png(size: int) -> Image.Image:
"""Full-bleed ink with the mark at 60%, so any launcher's crop still lands
on the whole drawing."""
canvas = Image.new("RGBA", (size, size), (*_rgb(INK), 255))
art = int(size * 0.60)
offset = (size - art) // 2
# The ramp is built at the *mark's* size, not the canvas's. Building it at
# canvas size would paint a 60% mark out of the middle of the ramp and wash
# the colours out — the same mistake DIAGONAL exists to prevent.
grad = Image.fromarray(_gradient(art)).convert("RGBA")
canvas.paste(grad, (offset, offset), _mark_alpha(art, art / 100.0, 0, 0))
return canvas
def _radial_glow(width, height, cx, cy, rx, ry, color, alpha):
"""Soft ellipse of `color` at `alpha` (0-1), falloff squared."""
y, x = np.ogrid[:height, :width]
t = ((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2
a = np.clip(1.0 - t, 0, 1) ** 2 * alpha
layer = np.zeros((height, width, 4), np.uint8)
layer[..., 0], layer[..., 1], layer[..., 2] = color
layer[..., 3] = (a * 255).astype(np.uint8)
return Image.fromarray(layer, "RGBA")
def social_png(width=1280, height=640) -> Image.Image:
"""GitHub social preview and the site OG card. 1280x640.
Same lockup energy as the marketing page: ink, a violet→cyan glow, the
mark, the current headline, a short CLI strip. Important type stays inside
a padded safe area so crops (Twitter, iMessage) don't eat the chevrons.
"""
bar_h = 8
canvas = Image.new("RGB", (width, height), _rgb(INK))
rgba = canvas.convert("RGBA")
rgba = Image.alpha_composite(
rgba,
_radial_glow(width, height, width * 0.38, height * 0.18, 520, 280, _rgb(GRAD_FROM), 0.22),
)
rgba = Image.alpha_composite(
rgba,
_radial_glow(width, height, width * 0.72, height * 0.78, 480, 240, _rgb(GRAD_TO), 0.14),
)
canvas = rgba.convert("RGB")
pen = ImageDraw.Draw(canvas)
bar = np.linspace(0, 1, width)[None, :, None]
a, b = np.array(_rgb(GRAD_FROM), float), np.array(_rgb(GRAD_TO), float)
strip = np.repeat((a + (b - a) * bar).astype(np.uint8), bar_h, axis=0)
canvas.paste(Image.fromarray(strip), (0, height - bar_h))
def font(path, size):
try:
return ImageFont.truetype(path, size)
except OSError:
return ImageFont.load_default()
title_f = font("/usr/share/fonts/truetype/lato/Lato-Black.ttf", 108)
line_f = font("/usr/share/fonts/truetype/lato/Lato-Semibold.ttf", 34)
meta_f = font("/usr/share/fonts/truetype/lato/Lato-Regular.ttf", 22)
url_f = font("/usr/share/fonts/truetype/lato/Lato-Medium.ttf", 22)
def size_of(text, fnt):
box = pen.textbbox((0, 0), text, font=fnt)
return box[2] - box[0], box[3] - box[1]
def draw(text, fnt, fill, x, y):
y0 = pen.textbbox((0, 0), text, font=fnt)[1]
pen.text((x, y - y0), text, font=fnt, fill=fill)
return pen.textbbox((0, 0), text, font=fnt)[3] - y0
title = "CLIque"
pre, hi, post = "A ", "folder", " for every CLI on the box."
clis = "Claude Code · Grok · Gemini · Codex · OpenCode"
url = "useclique.dev"
tw, th = size_of(title, title_f)
line_w = size_of(pre, line_f)[0] + size_of(hi, line_f)[0] + size_of(post, line_f)[0]
cw, ch = size_of(clis, meta_f)
uw, uh = size_of(url, url_f)
gap1, gap2, gap3 = 14, 22, 18
text_w = max(tw, line_w, cw, uw)
line_h = size_of(hi, line_f)[1]
text_h = th + gap1 + line_h + gap2 + ch + gap3 + uh
icon_size = 268
icon = icon_png(icon_size, tile=False)
gap = 56
group_w = icon_size + gap + text_w
group_h = max(icon_size, text_h)
ox = (width - group_w) // 2
oy = (height - bar_h - group_h) // 2
canvas.paste(icon, (ox, oy + (group_h - icon_size) // 2), icon)
tx = ox + icon_size + gap
ty = oy + (group_h - text_h) // 2
y = ty
y += draw(title, title_f, (255, 255, 255), tx, y) + gap1
x = tx
x += size_of(pre, line_f)[0]
draw(pre, line_f, (200, 206, 214), tx, y)
draw(hi, line_f, _rgb(SOLID), x, y)
draw(post, line_f, (200, 206, 214), x + size_of(hi, line_f)[0], y)
y += line_h + gap2
y += draw(clis, meta_f, (120, 128, 142), tx, y) + gap3
draw(url, url_f, _rgb(SOLID), tx, y)
return canvas
def write_ico(path: Path, sizes=(16, 32, 48, 64)) -> None:
"""Hand-rolled ICO: Pillow's writer re-encodes from one source and softens
the small sizes. Each size is drawn at its own scale instead, which is the
difference between a legible 16px favicon and a smudge."""
images = []
for size in sizes:
# Below 20px the tile and its rounding eat the mark, so the small sizes
# drop the tile and draw the chevrons alone, edge to edge.
img = small_icon_png(size) if size <= 20 else icon_png(size)
images.append(img)
entries, blobs, offset = [], [], 6 + 16 * len(images)
for size, img in zip(sizes, images, strict=True):
rgba = img.tobytes("raw", "BGRA")
rows = b"".join(
rgba[(size - 1 - y) * size * 4 : (size - y) * size * 4] for y in range(size)
)
mask = b"\x00" * (((size + 31) // 32) * 4 * size)
header = struct.pack(
"<IiiHHIIiiII", 40, size, size * 2, 1, 32, 0, len(rows) + len(mask), 0, 0, 0, 0
)
blob = header + rows + mask
entries.append(
struct.pack("<BBBBHHII", size % 256, size % 256, 0, 0, 1, 32, len(blob), offset)
)
offset += len(blob)
blobs.append(blob)
path.write_bytes(struct.pack("<HHH", 0, 1, len(images)) + b"".join(entries) + b"".join(blobs))
def write_png(path: Path, img: Image.Image) -> None:
img.save(path, "PNG", optimize=True)
def main() -> None:
OUT.mkdir(parents=True, exist_ok=True)
# Vector, in the three shapes anything ever needs.
(OUT / "logo.svg").write_text(svg_mark(square=True, gradient=True))
(OUT / "mark.svg").write_text(svg_mark(square=False, gradient=True))
(OUT / "mark-mono.svg").write_text(svg_mark(square=False, gradient=False))
(OUT / "lockup.svg").write_text(svg_lockup())
# Raster, for the places that cannot take an SVG.
for size in (16, 32, 64, 128, 180, 192, 256, 512):
art = small_icon_png(size) if size <= 20 else icon_png(size)
write_png(OUT / f"icon-{size}.png", art)
write_png(OUT / "apple-touch-icon.png", icon_png(180))
write_png(OUT / "icon-maskable-512.png", maskable_png(512))
write_png(OUT / "social-preview.png", social_png())
write_ico(OUT / "favicon.ico")
# Browsers ask for /favicon.ico at the root whatever the markup says, and
# behind `tailscale serve` that request never reaches a subdirectory. Keep
# a copy where they will actually look for it.
(OUT.parent / "favicon.ico").write_bytes((OUT / "favicon.ico").read_bytes())
for f in sorted(OUT.iterdir()):
print(f" {f.name:28} {f.stat().st_size:>7,} bytes")
if __name__ == "__main__":
main()