aboutsummaryrefslogtreecommitdiffstats
path: root/brand/make_logo.py
diff options
context:
space:
mode:
Diffstat (limited to 'brand/make_logo.py')
-rw-r--r--brand/make_logo.py235
1 files changed, 235 insertions, 0 deletions
diff --git a/brand/make_logo.py b/brand/make_logo.py
new file mode 100644
index 0000000..dac08b5
--- /dev/null
+++ b/brand/make_logo.py
@@ -0,0 +1,235 @@
+# -*- coding: utf-8 -*-
+"""Genera el kit de marca de 100cIA. El texto se convierte a curvas con Inkscape,
+asi que los SVG no dependen de que la fuente este instalada."""
+import os, re, subprocess, tempfile
+import xml.etree.ElementTree as ET
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+
+BG = "#0B0F1A"
+PUNTOS = "#243050"
+BLANCO = "#FFFFFF"
+AMBAR = "#FFD166"
+TRACK = "#1B2233"
+GRIS = "#8D99AE"
+
+FUENTE = "Roboto Black"
+TRACKING = -0.035 # em
+
+
+def curvas(partes, size=400, tracking=TRACKING):
+ """partes = [(texto, color), ...] -> (svg_interno, x, y, w, h) ya en curvas."""
+ tspans = "".join(
+ f'<tspan fill="{c}"{f" dx=\'{0.045 * size:g}\'" if i else ""}>{t}</tspan>'
+ for i, (t, c) in enumerate(partes))
+ src = (f'<svg xmlns="http://www.w3.org/2000/svg" width="6000" height="1200" '
+ f'viewBox="0 0 6000 1200"><text id="w" x="60" y="800" '
+ f'font-family="{FUENTE}" font-weight="900" font-size="{size}" '
+ f'letter-spacing="{tracking * size:g}">{tspans}</text></svg>')
+ with tempfile.TemporaryDirectory() as d:
+ a, b = os.path.join(d, "a.svg"), os.path.join(d, "b.svg")
+ open(a, "w", encoding="utf-8").write(src)
+ subprocess.run(["inkscape", "--export-text-to-path", "--export-plain-svg",
+ f"--export-filename={b}", a],
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
+ raw = open(b, encoding="utf-8").read()
+ q = {k: float(subprocess.run(["inkscape", "--query-id=w", f"--query-{k}", b],
+ capture_output=True, text=True).stdout.strip())
+ for k in ("x", "y", "width", "height")}
+
+ NS = "http://www.w3.org/2000/svg"
+ ET.register_namespace("", NS)
+ root = ET.fromstring(raw)
+ nodo = next(e for e in root.iter() if e.get("id") == "w")
+ for e in nodo.iter():
+ e.attrib.pop("id", None)
+ e.attrib.pop("aria-label", None)
+ cuerpo = ET.tostring(nodo, encoding="unicode")
+ cuerpo = cuerpo.replace(f' xmlns="{NS}"', "")
+ if not cuerpo.lstrip().startswith("<g"):
+ cuerpo = f"<g>{cuerpo}</g>"
+ return cuerpo, q["x"], q["y"], q["width"], q["height"]
+
+
+def colocar(g, bx, by, bw, bh, cx, cy, ancho):
+ s = ancho / bw
+ tx = cx - s * (bx + bw / 2)
+ ty = cy - s * (by + bh / 2)
+ return f'<g transform="translate({tx:.3f},{ty:.3f}) scale({s:.5f})">{g}</g>'
+
+
+def puntos(id_, sep=118, r=4.2, color=PUNTOS, op=0.55):
+ return (f'<pattern id="{id_}" width="{sep}" height="{sep}" '
+ f'patternUnits="userSpaceOnUse">'
+ f'<circle cx="{sep/2}" cy="{sep/2}" r="{r}" fill="{color}" '
+ f'opacity="{op}"/></pattern>')
+
+
+def arco(cx, cy, r, desde, hasta, color, w):
+ """Arco en grados, 0 = arriba, sentido horario."""
+ import math
+ def p(a):
+ t = math.radians(a - 90)
+ return cx + r * math.cos(t), cy + r * math.sin(t)
+ x0, y0 = p(desde)
+ x1, y1 = p(hasta)
+ largo = 1 if (hasta - desde) % 360 > 180 else 0
+ return (f'<path d="M {x0:.2f} {y0:.2f} A {r} {r} 0 {largo} 1 {x1:.2f} {y1:.2f}" '
+ f'fill="none" stroke="{color}" stroke-width="{w}" stroke-linecap="round"/>')
+
+
+# ---------------------------------------------------------------- avatar
+def avatar(archivo, chip=False, pct=0.62):
+ D = 1024
+ R = D / 2
+ anillo_r, anillo_w = R - 26, 20
+ if chip:
+ g1, x1, y1, w1, h1 = curvas([("100c", BLANCO)])
+ g2, x2, y2, w2, h2 = curvas([("IA", "#0B0F1A")])
+ else:
+ g1, x1, y1, w1, h1 = curvas([("100c", BLANCO), ("IA", AMBAR)])
+
+ cuerpo = [
+ f'<circle cx="{R}" cy="{R}" r="{R}" fill="{BG}"/>',
+ f'<circle cx="{R}" cy="{R}" r="{R}" fill="url(#dots)"/>',
+ f'<circle cx="{R}" cy="{R}" r="{anillo_r}" fill="none" '
+ f'stroke="{TRACK}" stroke-width="{anillo_w}"/>',
+ arco(R, R, anillo_r, 0, 360 * pct, AMBAR, anillo_w),
+ ]
+ if chip:
+ ancho_total = 760
+ # el chip de "IA" ocupa ~38% del ancho, como los chips de los videos
+ w_txt = ancho_total * 0.60
+ cuerpo.append(colocar(g1, x1, y1, w1, h1, R - ancho_total / 2 + w_txt / 2, R, w_txt))
+ esc = w_txt / w1
+ cw, ch = w2 * esc * 1.0 + 70, h1 * esc + 62
+ cxc = R - ancho_total / 2 + w_txt + 26 + cw / 2
+ cuerpo.append(f'<rect x="{cxc - cw/2:.1f}" y="{R - ch/2:.1f}" width="{cw:.1f}" '
+ f'height="{ch:.1f}" rx="{ch*0.30:.1f}" fill="{AMBAR}"/>')
+ cuerpo.append(colocar(g2, x2, y2, w2, h2, cxc, R, w2 * esc))
+ else:
+ cuerpo.append(colocar(g1, x1, y1, w1, h1, R, R, 740))
+
+ svg = (f'<svg xmlns="http://www.w3.org/2000/svg" width="{D}" height="{D}" '
+ f'viewBox="0 0 {D} {D}"><defs>{puntos("dots")}</defs>'
+ + "".join(cuerpo) + '</svg>')
+ open(os.path.join(HERE, archivo), "w", encoding="utf-8").write(svg)
+ return archivo
+
+
+# ---------------------------------------------------------------- logotipo
+# El chip alrededor del "IA" es la marca: va en todas las variantes menos en la
+# "plana", que queda como alternativa.
+MODOS = {
+ # tinta fondo del chip letra del chip
+ "oscuro": (BLANCO, AMBAR, "#0B0F1A"),
+ "claro": ("#0B0F1A", "#D98C00", "#FFFFFF"),
+ "mono": (BLANCO, BLANCO, None), # letras caladas
+}
+
+
+def _lockup(modo, alto=520):
+ """Devuelve (piezas_svg, ancho, alto) del lockup '100c' + chip 'IA'."""
+ tinta, fondo_chip, letra = MODOS[modo]
+ g1, x1, y1, w1, h1 = curvas([("100c", tinta)])
+ g2, x2, y2, w2, h2 = curvas([("IA", letra or "#000000")])
+
+ esc = alto / h1
+ wt = w1 * esc
+ cw = w2 * esc + 0.68 * alto
+ ch = alto + 0.60 * alto
+ hueco = 0.13 * alto
+ W = wt + hueco + cw
+ cy = ch / 2
+ rx = ch * 0.28
+ cx_chip = wt + hueco + cw / 2
+ chip_x = wt + hueco
+
+ piezas = [colocar(g1, x1, y1, w1, h1, wt / 2, cy, wt)]
+ if letra is None: # una sola tinta: el IA se cala
+ mascara = (f'<mask id="cala">'
+ f'<rect x="{chip_x:.1f}" y="0" width="{cw:.1f}" height="{ch:.1f}" '
+ f'rx="{rx:.1f}" fill="#FFFFFF"/>'
+ + colocar(g2, x2, y2, w2, h2, cx_chip, cy, w2 * esc)
+ + '</mask>')
+ piezas.append(f'<defs>{mascara}</defs>')
+ piezas.append(f'<rect x="{chip_x:.1f}" y="0" width="{cw:.1f}" height="{ch:.1f}" '
+ f'rx="{rx:.1f}" fill="{fondo_chip}" mask="url(#cala)"/>')
+ else:
+ piezas.append(f'<rect x="{chip_x:.1f}" y="0" width="{cw:.1f}" height="{ch:.1f}" '
+ f'rx="{rx:.1f}" fill="{fondo_chip}"/>')
+ piezas.append(colocar(g2, x2, y2, w2, h2, cx_chip, cy, w2 * esc))
+ return piezas, W, ch
+
+
+def wordmark(archivo, modo="oscuro", margen=0.10):
+ piezas, W0, H0 = _lockup(modo)
+ m = margen * H0
+ W, H = int(round(W0 + 2 * m)), int(round(H0 + 2 * m))
+ svg = (f'<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" '
+ f'viewBox="0 0 {W} {H}"><g transform="translate({m:.1f},{m:.1f})">'
+ + "".join(piezas) + '</g></svg>')
+ open(os.path.join(HERE, archivo), "w", encoding="utf-8").write(svg)
+ return archivo
+
+
+def wordmark_plano(archivo):
+ """Alternativa sin chip: '100cIA' de corrido, el IA en ambar."""
+ g, x, y, w, h = curvas([("100c", BLANCO), ("IA", AMBAR)])
+ W = 2400
+ ancho = W * 0.88
+ H = int(round(h / w * ancho + 2 * 0.06 * W))
+ svg = (f'<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" '
+ f'viewBox="0 0 {W} {H}">'
+ + colocar(g, x, y, w, h, W / 2, H / 2, ancho) + '</svg>')
+ open(os.path.join(HERE, archivo), "w", encoding="utf-8").write(svg)
+ return archivo
+
+
+# ---------------------------------------------------------------- banner
+def banner(archivo, tagline="ciencia en 40 segundos"):
+ W, H = 1920, 1080
+ g, x, y, w, h = curvas([("100c", BLANCO)])
+ t, tx, ty, tw, th = curvas([(tagline, "#C9D3E5")], size=200, tracking=0.012)
+ cuerpo = [
+ f'<rect width="{W}" height="{H}" fill="{BG}"/>',
+ f'<rect width="{W}" height="{H}" fill="url(#dots)"/>',
+ f'<rect x="0" y="0" width="{W}" height="12" fill="{TRACK}"/>',
+ f'<rect x="0" y="0" width="{int(W*0.62)}" height="12" fill="{AMBAR}"/>',
+ None, # el lockup se arma abajo (con chip, igual que el avatar)
+ colocar(t, tx, ty, tw, th, W / 2, H / 2 + 205, 830),
+ ]
+ # lockup con chip, centrado, a 1150 px de ancho total
+ g2, x2, y2, w2, h2 = curvas([("IA", "#0B0F1A")])
+ ANCHO, cy = 1150, H / 2 - 95
+ alto = 210
+ esc = alto / h
+ wt = w * esc
+ cw, ch = w2 * esc + 0.68 * alto, alto + 0.60 * alto
+ hueco = 0.13 * alto
+ total = wt + hueco + cw
+ k = ANCHO / total
+ wt, cw, ch, hueco = wt * k, cw * k, ch * k, hueco * k
+ izq = W / 2 - ANCHO / 2
+ cuerpo[cuerpo.index(None)] = (
+ colocar(g, x, y, w, h, izq + wt / 2, cy, wt)
+ + f'<rect x="{izq + wt + hueco:.1f}" y="{cy - ch/2:.1f}" width="{cw:.1f}" '
+ f'height="{ch:.1f}" rx="{ch*0.28:.1f}" fill="{AMBAR}"/>'
+ + colocar(g2, x2, y2, w2, h2, izq + wt + hueco + cw / 2, cy, w2 * esc * k))
+
+ svg = (f'<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" '
+ f'viewBox="0 0 {W} {H}"><defs>{puntos("dots", sep=140, r=5)}</defs>'
+ + "".join(cuerpo) + '</svg>')
+ open(os.path.join(HERE, archivo), "w", encoding="utf-8").write(svg)
+ return archivo
+
+
+if __name__ == "__main__":
+ # la variante con chip es la marca: se queda con los nombres principales
+ for f in (avatar("avatar.svg", chip=True), avatar("avatar-plano.svg"),
+ wordmark("wordmark.svg", "oscuro"),
+ wordmark("wordmark-claro.svg", "claro"),
+ wordmark("wordmark-mono.svg", "mono"),
+ wordmark_plano("wordmark-plano.svg"),
+ banner("banner.svg")):
+ print(" ->", f)