From f65e7ae61541b4e564697259bda3c471e5ee5b74 Mon Sep 17 00:00:00 2001 From: Lucas Rayan Date: Sun, 18 Oct 2026 21:51:44 -0300 Subject: [PATCH] =?UTF-8?q?Nova=20logo=20em=20estilo=20geom=C3=A9trico=20f?= =?UTF-8?q?acetado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cabeça do lobo-guará contornada sobre foto de referência, com as regiões de cor do animal real em tons de verde-azulado e o pescoço se desfazendo em estilhaços. Co-Authored-By: Claude Opus 5.5 --- apps/gui/ui/guara-logo.svg | 2 +- apps/gui/ui/guara.svg | 2 +- scripts/make-logo.py | 363 ++++++++++++------------------------- 3 files changed, 122 insertions(+), 245 deletions(-) diff --git a/apps/gui/ui/guara-logo.svg b/apps/gui/ui/guara-logo.svg index 73a7f55..09a34ae 100644 --- a/apps/gui/ui/guara-logo.svg +++ b/apps/gui/ui/guara-logo.svg @@ -1 +1 @@ - + diff --git a/apps/gui/ui/guara.svg b/apps/gui/ui/guara.svg index 74ce45b..2da20c6 100644 --- a/apps/gui/ui/guara.svg +++ b/apps/gui/ui/guara.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/scripts/make-logo.py b/scripts/make-logo.py index 2bc858c..7208f09 100644 --- a/scripts/make-logo.py +++ b/scripts/make-logo.py @@ -1,251 +1,128 @@ #!/usr/bin/env python3 -"""Gera a identidade visual do Guará. +"""Gera a logo do Guará em estilo geométrico facetado (low-poly). -Conceito: pixels de tamanhos distintos, soltos à esquerda, que se juntam numa -grade cada vez mais densa até formar a cabeça de um lobo-guará à direita. +A cabeça do lobo-guará foi contornada sobre uma foto de referência, com as +regiões de cor do animal real: focinho escuro subindo até entre os olhos, juba +escura na nuca, interior das orelhas e garganta claros. As facetas (Delaunay) +seguem essas regiões, e o pescoço se solta em estilhaços para a esquerda. -- O lobo é uma silhueta VETORIAL suave (curvas), sutil, sem serrilhado. -- À esquerda a silhueta é recortada por uma máscara de quadrados de tamanhos - distintos que ficam cada vez mais densos até se fundirem no lobo inteiro. -- Partículas soltas (quadrados) completam a dissolução fora da silhueta. - -Uso: python3 scripts/make-logo.py -> apps/gui/ui/guara-logo.svg e guara.svg +Uso: python3 scripts/make-logo.py recorte.png [saida.svg] +Requer numpy, scipy e pillow. O ícone (guara.svg) embute esta mesma arte. """ -import math -import random -from pathlib import Path - -OUT = Path(__file__).resolve().parent.parent / "apps/gui/ui" - -W, H = 160.0, 100.0 -CELL = 2.5 # módulo da grade de pixels -COLS, ROWS = int(W / CELL), int(H / CELL) - -# --- silhueta ------------------------------------------------------------- -# Cabeça de lobo-guará (3/4, olhando para a direita): orelhas enormes, focinho -# longo e fino, e a juba escorrendo para a esquerda (é ela que se desmancha em -# pixels). Coordenadas 0..100 na altura; x pode ser negativo (juba). -WOLF = [ - (100, 57), (97.5, 54), (91, 51), (83, 48), (75, 45), (69, 41), (65, 36), (62, 29), (59, 20), (56, 11), - (52.5, 4), (49, 0.5), (46, 4), (43, 13), (41, 23), (40, 32), (38, 31), (35, 22), (31, 11), (27.5, 3), - (24.5, 0.5), (22, 5), (20, 15), (19, 27), (9, 35), (-6, 45), (-20, 58), (-30, 76), (-34, 100), (-10, 100), (20, 100), (44, 100), - (60, 100), (59.2, 95), (59.6, 89), (62, 82), (65, 78), (72, 74), (82, 71.5), (92, 69), (98.5, 66.5), (100, 63), +import math, random, sys +import numpy as np +from PIL import Image +from scipy.spatial import Delaunay +OUT=[(98,382),(104,350),(110,318),(114,290),(116,262),(110,235),(100,212), +(84,178),(66,140),(54,100),(50,68),(54,44),(70,52),(98,70),(128,94),(156,118),(176,146), +(180,112),(190,78),(204,50),(216,32),(230,40),(252,64),(274,94),(292,120),(305,135), +(330,138),(370,142),(420,150),(500,165),(500,460),(320,460),(300,420),(260,385),(225,372),(190,380),(150,398),(118,404),(102,396)] +EARS=[[(52,78),(80,96),(115,118),(150,142),(130,170),(105,185),(85,160),(65,120)], + [(222,48),(245,62),(270,95),(285,125),(255,165),(215,165),(195,130),(200,85)]] +EYES=[(125,270),(200,268)] +ZONES=[ + # interior claro das orelhas (com borda verde em volta) + ('pale',[(62,62),(92,78),(122,100),(150,124),(160,144),(138,166),(116,190),(98,166),(76,124),(62,92)]), + ('pale',[(214,50),(228,56),(248,78),(268,106),(282,128),(258,150),(226,160),(200,146),(196,112),(204,76)]), + # testa um pouco mais clara entre as orelhas + ('brow',[(160,150),(185,148),(240,170),(270,190),(250,220),(200,230),(160,225),(130,205)]), + # área dos olhos, um tom mais escuro + ('mask',[(108,250),(140,250),(165,246),(195,250),(222,262),(220,284),(196,292),(165,282),(140,290),(112,290)]), + # juba escura na nuca + ('mane',[(298,136),(500,166),(500,236),(430,214),(360,198),(318,182),(300,160)]), + # garganta clara e linha clara do lábio + ('pale',[(214,350),(234,332),(266,324),(302,320),(316,344),(306,372),(274,388),(240,380),(220,370)]), + ('pale',[(150,398),(186,382),(212,368),(222,374),(204,388),(166,404)]), + # focinho preto subindo pelo dorso do nariz até entre os olhos + ('muzzle',[(96,384),(102,352),(108,322),(114,296),(132,284),(150,266),(164,258),(176,268),(184,294),(196,322),(206,352),(200,374),(170,392),(130,404),(104,398)]), ] -# Interior das orelhas (escuro, como no animal real). -EAR_IN = [ - [(53, 9), (50, 3), (47, 10), (45, 20), (47, 27), (51, 24), (54, 15)], - [(27.5, 8), (25, 4), (23, 14), (22, 24), (25, 27), (28, 20)], -] -WX0, WSCALE = 64.0, 88.0 # nariz em x=152 (margem de 8 à direita) -Y0, YS = 6.0, 88.0 # topo das orelhas em y=6, base em y=94 # x do desenho onde wx=0 e largura de 100 unidades do lobo - -def to_wolf(x, y): - return (x - WX0) * 100.0 / WSCALE, y * 100.0 / H - - -def wolf_at(x, y): - wx, wy = to_wolf(x, y) - if not _inside(WOLF, wx, wy): - return False - return math.hypot(wx - 71.0, wy - 49.0) >= 1.7 # olho - - -def ear_in(x, y): - wx, wy = to_wolf(x, y) - return any(_inside(p, wx, wy) for p in EAR_IN) - - -def _inside(poly, x, y): - c = False - j = len(poly) - 1 +from PIL import ImageFilter +REF = sys.argv[1] # recorte da cabeça da foto de referência (390x460), não versionado +img=np.array(Image.open(REF).convert('RGB').filter(ImageFilter.MedianFilter(9)).filter(ImageFilter.GaussianBlur(4))).astype(float) +Hc,Wc=img.shape[:2] +rnd=random.Random(7) +# contorno: estende o pescoço para a esquerda (depois do espelhamento) +C=[p for p in OUT if p[0]<=500] +def inside(poly,x,y): + c=False; j=len(poly)-1 for i in range(len(poly)): - xi, yi = poly[i] - xj, yj = poly[j] - if (yi > y) != (yj > y) and x < (xj - xi) * (y - yi) / (yj - yi) + xi: - c = not c - j = i + xi,yi=poly[i]; xj,yj=poly[j] + if (yi>y)!=(yj>y) and x<(xj-xi)*(y-yi)/(yj-yi)+xi: c=not c + j=i return c - - -def smooth(t): - t = max(0.0, min(1.0, t)) - return t * t * (3 - 2 * t) - - -def mix(c1, c2, t): - t = max(0.0, min(1.0, t)) - a = [int(c1[i:i + 2], 16) for i in (1, 3, 5)] - b = [int(c2[i:i + 2], 16) for i in (1, 3, 5)] - return "#" + "".join(f"{round(p + (q - p) * t):02x}" for p, q in zip(a, b)) - - -def smooth_path(pts, dup=()): - """Curva fechada (Catmull-Rom -> Bézier); `dup` = índices de quinas vivas.""" - P = [] - for i, p in enumerate(pts): - P.append(p) - if i in dup: - P.append(p) - n = len(P) - d = f"M{P[0][0]:.2f},{P[0][1]:.2f}" - for i in range(n): - p0, p1, p2, p3 = P[(i - 1) % n], P[i], P[(i + 1) % n], P[(i + 2) % n] - c1 = (p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6) - c2 = (p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6) - d += f"C{c1[0]:.2f},{c1[1]:.2f} {c2[0]:.2f},{c2[1]:.2f} {p2[0]:.2f},{p2[1]:.2f}" - return d + "Z" - - -def tx(pts): - return [(WX0 + x * WSCALE / 100.0, Y0 + y * YS / 100.0) for x, y in pts] - - -def lerp(a, b, t): - return a + (b - a) * t - - -def build(pal, scatter=1.0, seed=5, uid="a", x_min=8.0): - rnd = random.Random(seed) - wolf = tx(WOLF) - corners = {i for i, (_, y) in enumerate(WOLF) if y >= 99.5 or y <= 0.6} # pontas e cantos vivos - body = smooth_path(wolf, dup=corners) - ears = [smooth_path(tx(e)) for e in EAR_IN] - cols, rows = int(W / CELL), int(H / CELL) - - # gradiente da figura (o mesmo para a silhueta e para cada pixel => fusão sem emenda) - gx0, gy0, gx1, gy1 = 58.0, 0.0, 160.0, 100.0 - stops = pal["stops"] - - def grad(x, y): - dx, dy = gx1 - gx0, gy1 - gy0 - t = max(0.0, min(1.0, ((x - gx0) * dx + (y - gy0) * dy) / (dx * dx + dy * dy))) - for (t0, c0), (t1, c1) in zip(stops, stops[1:]): - if t <= t1: - return mix(c0, c1, (t - t0) / (t1 - t0)) - return stops[-1][1] - - X_SOLID = 80.0 # múltiplo do módulo: a partir daqui a figura é inteira e lisa - - def ramp(x): - return min(1.0, max(0.0, x / X_SOLID)) ** 2.0 - - # envelope da "cauda": nasce na largura do pescoço e se afunila para a esquerda - def envelope(x, y): - k = smooth((x + 6) / 80.0) - yc = lerp(70, 62, k) - hh = lerp(5, 31, k ** 1.15) - return math.exp(-(abs(y - yc) / hh) ** 3.0) - - occ = set() - - def free(cx, cy, n, m=0): - return all((cx + i, cy + j) not in occ for i in range(-m, n + m) for j in range(-m, n + m)) - - def take(cx, cy, n): - for i in range(n): - for j in range(n): - occ.add((cx + i, cy + j)) - - mask_sq, free_sq = [], [] - - def emit(cx, cy, n, sz_scale, op, gap): - x0, y0 = cx * CELL, cy * CELL - size = n * CELL - sz = size * sz_scale - off = (size - sz) / 2 - cxm, cym = x0 + size / 2, y0 + size / 2 - inside = _inside(wolf, cxm, cym) - if inside and x0 >= 56: - # faz parte da figura lisa: vira janela da máscara - e = 0.12 - mask_sq.append(f'') - else: - free_sq.append(f'') - - # 1) campo de pixels da esquerda até a figura: tamanhos em múltiplos do módulo - order = [(cx, cy) for cy in range(rows) for cx in range(int(x_min / CELL), int(X_SOLID / CELL))] - rnd.shuffle(order) - for cx, cy in order: - x, y = (cx + 0.5) * CELL, (cy + 0.5) * CELL - r, env = ramp(x), envelope(x, y) - p = r * env - if p < 0.01 or y > 94: - continue - # blocos grandes na faixa média; sempre 1×1 colados perto da figura - n = 1 - if r < 0.7: - big = rnd.random() < 0.9 * (1 - r) * env ** 0.5 * min(1.0, r * 3.2) - if big: - n = rnd.choice([2, 2, 3]) if r < 0.45 else 2 - if cx + n > cols or cy + n > rows or not free(cx, cy, n, 0): - n = 1 - if not free(cx, cy, 1, 0): - continue - acc = p if n == 1 else min(1.0, p * 1.6) - if r > 0.88: - acc = 1.0 if env > 0.45 else acc - if rnd.random() > acc: - continue - take(cx, cy, n) - gap_scale = 1.0 if r > 0.86 else (0.9 - 0.45 * (1 - r)) if n > 1 else rnd.choice([0.5, 0.65, 0.8, 0.9]) * (0.7 + 0.3 * r) - op = 0.38 + 0.62 * smooth(r * 1.25) - emit(cx, cy, n, gap_scale if r <= 0.86 else 1.02, op, 0) - - # 2) partículas esparsas bem à esquerda e acima/abaixo da cauda (profundidade) - placed, tries, target = 0, 0, int(34 * scatter) - while placed < target and tries < 6000: - tries += 1 - x = x_min + rnd.random() ** 1.3 * (70 - x_min) - cy = rnd.randrange(3, rows - 4) - cx = int(x / CELL) - y = (cy + 0.5) * CELL - env = envelope(x, y) - if env > 0.5 or not free(cx, cy, 1, 1): - continue - r = ramp(x) - # sempre perto da cauda, nunca longe demais - near = math.exp(-((abs(y - 62) - 26) / 22.0) ** 2) - if rnd.random() > 0.55 * near * (0.35 + 0.65 * r): - continue - take(cx, cy, 1) - emit(cx, cy, 1, rnd.choice([0.45, 0.6, 0.75]), 0.22 + 0.38 * r, 0) - placed += 1 - - ex, ey = tx([(71.0, 49.0)])[0] - hx = " ".join(f'' for t, c in stops) - svg = ( - "" - f'{hx}' - f'' - f'' - + "".join(mask_sq) - + "" - f'' - + "".join(f'' for e in ears) - + "" - + '' + "".join(free_sq) + "" - + f'' - ) - return svg - - -def main(): - # Marca larga: funciona em fundo claro e escuro. Verde-azulado -> azul petróleo. - pal = dict(stops=[(0.0, "#3BE3CC"), (0.45, "#12B5A8"), (1.0, "#0B6A86")], ear="#05323a", ear_op=0.5, eye="#ecfeff") - (OUT / "guara-logo.svg").write_text( - f'' + build(pal, uid="w") + "\n" - ) - # Ícone: fundo petróleo profundo, figura clara. - ipal = dict(stops=[(0.0, "#d9fff7"), (0.5, "#8df1e0"), (1.0, "#3fc8cf")], ear="#0b4a55", ear_op=0.45, eye="#083344") - (OUT / "guara.svg").write_text( - '' - '' - '' - '' - '' + build(ipal, scatter=0.5, seed=9, uid="i", x_min=30.0) + "\n" - ) - - -if __name__ == "__main__": - main() +# pontos: contorno reamostrado + traços anatômicos + preenchimento com jitter +pts=[] +for i in range(len(C)): + a=C[i]; b=C[(i+1)%len(C)]; n=max(1,int(math.dist(a,b)/16)) + for k in range(n): pts.append((a[0]+(b[0]-a[0])*k/n, a[1]+(b[1]-a[1])*k/n)) +FEAT=[(125,270),(200,268),(110,300),(160,286),(185,300),(100,383),(118,404),(150,396),(200,360), + (215,330),(260,322),(300,318),(310,350),(270,386),(140,140),(110,190),(80,120),(255,100),(270,150),(225,160), + (205,110),(175,180),(230,230),(150,230),(300,200),(340,250),(320,170),(360,330),(400,260),(450,330),(260,280)] +pts+=FEAT +# vértices nas bordas das regiões: as facetas seguem os contornos das cores +for k_,Z in ZONES: + for i in range(len(Z)): + a=Z[i]; b=Z[(i+1)%len(Z)]; n=max(1,int(math.dist(a,b)/18)) + for j in range(n): + q=(a[0]+(b[0]-a[0])*j/n, a[1]+(b[1]-a[1])*j/n) + if inside(C,*q) and min(math.dist(q,p_) for p_ in pts)>6: pts.append(q) +sp=24 +for y in np.arange(20,470,sp): + for x in np.arange(20,510,sp): + jx,jy=x+rnd.uniform(-8,8),y+rnd.uniform(-8,8) + if inside(C,jx,jy) and min(math.dist((jx,jy),p) for p in pts)>15: pts.append((jx,jy)) +P=np.array(pts) +tri=Delaunay(P) +def sample(t): + a,b,c=P[t] + cols=[] + for _ in range(40): + u,v=rnd.random(),rnd.random() + if u+v>1: u,v=1-u,1-v + x,y=a+(b-a)*u+(c-a)*v + x=min(max(x,0),Wc-1); y=min(max(y,0),Hc-1) + if x>380: x=380-(x-380)*0.2 # o pescoço além da foto herda a cor da borda + cols.append(img[int(y),int(x)]) + m=np.mean(cols,axis=0); return m +RAMP=["#083a45","#0f766e","#14b8a6","#5eead4","#c9faf1"] +def tone(rgb): + r,g,b=rgb; L=(0.299*r+0.587*g+0.114*b)/255 + TH=[0.17,0.30,0.50,0.64] + return RAMP[sum(L>t for t in TH)] +def hx(c): return tuple(int(c[i:i+2],16) for i in (1,3,5)) +def mix(c1,c2,t): + a,b=hx(c1),hx(c2); return "#"+"".join(f"{round(p+(q-p)*t):02x}" for p,q in zip(a,b)) +BASE={'muzzle':("#07323c","#0d5563"),'mane':("#0b4f5c","#0f766e"),'pale':("#9ef0e2","#e3fdf8"),'face':("#0f9e94","#3fdcc8"),'brow':("#1fb9a9","#52dccb"),'mask':("#0c7d76","#139c92")} +def zone_of(x,y): + for k,P_ in reversed(ZONES): + if inside(P_,x,y): return k + return 'face' +F=[] +for t in tri.simplices: + cx,cy=P[t].mean(axis=0) + if not inside(C,cx,cy): continue + z=zone_of(cx,cy) + lo,hi=BASE[z] + # luz vinda do alto e da frente (focinho), com variação sutil por faceta + light=0.55*(1-cy/470)+0.35*(1-min(1,cx/420))+rnd.uniform(-0.12,0.12) + F.append((t, mix(lo,hi,max(0,min(1,light))), cx, cy)) +# espelha e encaixa no quadro 160x100 da logo +S=92/430 +def T(x,y): return (150-(x-38)*S, 4+(y-30)*S) +out=[] +for t,col,cx,cy in F: + q=[T(*P[i]) for i in t] + # estilhaços: o pescoço (esquerda) se solta e se desfaz + gx=T(cx,cy)[0] + k=max(0,(78-gx)/40) + if k>0 and rnd.random()') +eyes="".join(f'' for x,y in EYES) +open(sys.argv[2] if len(sys.argv) > 2 else 'guara-logo.svg','w').write(f'{"".join(out)}{eyes}\n')