diff --git a/apps/gui/src-tauri/icons/128x128.png b/apps/gui/src-tauri/icons/128x128.png index 98e2dc6..4ca70f7 100644 Binary files a/apps/gui/src-tauri/icons/128x128.png and b/apps/gui/src-tauri/icons/128x128.png differ diff --git a/apps/gui/src-tauri/icons/128x128@2x.png b/apps/gui/src-tauri/icons/128x128@2x.png index d1768e8..5d0b22e 100644 Binary files a/apps/gui/src-tauri/icons/128x128@2x.png and b/apps/gui/src-tauri/icons/128x128@2x.png differ diff --git a/apps/gui/src-tauri/icons/32x32.png b/apps/gui/src-tauri/icons/32x32.png index aae93ae..d1faf21 100644 Binary files a/apps/gui/src-tauri/icons/32x32.png and b/apps/gui/src-tauri/icons/32x32.png differ diff --git a/apps/gui/src-tauri/icons/icon.ico b/apps/gui/src-tauri/icons/icon.ico index 297ce64..cbb8e7c 100644 Binary files a/apps/gui/src-tauri/icons/icon.ico and b/apps/gui/src-tauri/icons/icon.ico differ diff --git a/apps/gui/src-tauri/icons/icon.png b/apps/gui/src-tauri/icons/icon.png index 025426f..a2bc21e 100644 Binary files a/apps/gui/src-tauri/icons/icon.png and b/apps/gui/src-tauri/icons/icon.png differ diff --git a/apps/gui/src-tauri/icons/source.png b/apps/gui/src-tauri/icons/source.png index 025426f..a2bc21e 100644 Binary files a/apps/gui/src-tauri/icons/source.png and b/apps/gui/src-tauri/icons/source.png differ diff --git a/apps/gui/ui/guara-logo.svg b/apps/gui/ui/guara-logo.svg index 258babd..73a7f55 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 382878a..74ce45b 100644 --- a/apps/gui/ui/guara.svg +++ b/apps/gui/ui/guara.svg @@ -1 +1 @@ - + diff --git a/scripts/make-logo.py b/scripts/make-logo.py index 28aa196..2bc858c 100644 --- a/scripts/make-logo.py +++ b/scripts/make-logo.py @@ -1,9 +1,15 @@ #!/usr/bin/env python3 -"""Gera a identidade visual do Guará: pixels de tamanhos distintos, soltos à esquerda, -que se juntam até formar a cabeça de um lobo-guará à direita. +"""Gera a identidade visual do Guará. -Uso: python3 scripts/make-logo.py (escreve apps/gui/ui/guara.svg e guara-logo.svg) -Determinístico (semente fixa): a mesma execução gera o mesmo desenho. +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. + +- 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 """ import math import random @@ -11,15 +17,46 @@ from pathlib import Path OUT = Path(__file__).resolve().parent.parent / "apps/gui/ui" -# Cabeça de lobo-guará de perfil, olhando para a direita (coordenadas 0..100). +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 = [ - (-18, 100), (-2, 76), (10, 62), (16, 54), (21, 2), (30, 24), (41, 40), (47, 38), (57, 0), (62, 34), (72, 46), - (88, 55), (100, 60), (100, 69), (88, 73), (74, 77), (68, 84), (62, 100), (40, 100), + (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), ] -EYE = (63, 54, 2.4) # x, y, raio (em coords do lobo) +# 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 inside(poly, x, y): +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 for i in range(len(poly)): @@ -31,76 +68,182 @@ def inside(poly, x, y): return c -def lerp(a, b, t): - return a + (b - a) * t - - -def hexmix(c1, c2, 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(lerp(x, y, t)):02x}" for x, y in zip(a, b)) - - def smooth(t): t = max(0.0, min(1.0, t)) return t * t * (3 - 2 * t) -def pixels(x_from, x_to, c_left, c_right, seed=7): +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) - cell = 4.0 - out = [] - wolf_x0, wolf_w = 62, 98 # onde o lobo começa e quanto mede (em unidades do desenho) - for cy in range(25): - for cx in range(40): - x, y = cx * cell + cell / 2, cy * cell + cell / 2 - if not (x_from <= x <= x_to): + 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 - t = smooth((x - 14) / 96) - wx, wy = (x - wolf_x0) * 100 / wolf_w, y - in_wolf = inside(WOLF, wx, wy) and math.hypot(wx - EYE[0], wy - EYE[1]) > EYE[2] - if in_wolf: - keep = 0.06 + 0.94 * t ** 1.15 - if t > 0.9: - keep = 1.0 - if rnd.random() > keep: - continue - size = cell * (0.28 + 0.72 * t ** 0.9) * (1 if t > 0.9 else rnd.uniform(0.65, 1.15)) - jit = (1 - t) ** 1.4 * cell * 0.9 - else: - if rnd.random() > 0.2 * (1 - t) ** 1.2 or x > 96: - continue - size = cell * rnd.uniform(0.22, 0.85) - jit = cell * 0.9 - size = min(size, cell * 1.05) - px = x - size / 2 + rnd.uniform(-jit, jit) - py = y - size / 2 + rnd.uniform(-jit, jit) - col = hexmix(c_left, c_right, max(0.0, min(1.0, t * 1.05 + rnd.uniform(-0.12, 0.12)))) - op = 0.5 + 0.5 * t if not in_wolf or t < 0.9 else 1 - out.append((px, py, size, col, op)) - return out + 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 -def rects(px, dx=0.0, dy=0.0): - return "".join( - f'' - for x, y, s, c, o in px + 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 (fundo transparente): ciano claro -> verde-azulado profundo. - wide = pixels(0, 160, "#67e8f9", "#0f766e") + # 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( - '' + rects(wide) + "\n" + f'' + build(pal, uid="w") + "\n" ) - # Ícone quadrado: fundo verde-azulado escuro, pixels claros. - icon_px = pixels(30, 160, "#99f6e4", "#f0fdfa", seed=11) + # Í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( '' - '' - '' - '' + rects(icon_px) + "\n" + '' + '' + '' + '' + build(ipal, scatter=0.5, seed=9, uid="i", x_min=30.0) + "\n" )