# Prompt: letreiro animado em gradiente, num SVG de ~3 KB

Cole este texto no seu agente (Claude Code, Cursor, o que você usar).

## O que eu quero

Um letreiro animado num único arquivo SVG de uns 3 KB, sem JavaScript, sem canvas e sem WebGL.
Uma faixa de luz varre a palavra sem parar, e cada tom de cinza vira uma cor da minha marca. O
miolo das letras fica escuro e a cor aparece na borda e onde a faixa passa. Quero uma versão pro
tema claro e outra pro escuro.

A técnica é de Mike Bespalov: https://x.com/bbssppllvv/status/2104747296671883312
Este prompt reproduz a versão que o Gabriel Souza fez pro PDR, e os arquivos dele servem de
gabarito: https://gsouza.xyz/writing/pdr-gradient/pdr-claro.svg e
https://gsouza.xyz/writing/pdr-gradient/pdr-escuro.svg

## Antes de começar, me pergunte

1. **A palavra** e **o arquivo da fonte** (.ttf ou .otf). Prefira fonte bem pesada (Bold, Black):
   o efeito acontece dentro do traço, e traço fino não segura.
2. **A rampa do tema claro**: de 8 a 16 cores em hex, do miolo da letra pra borda. A primeira é
   bem escura e a última é exatamente a cor do fundo onde o letreiro vai ficar.
3. **A rampa do tema escuro**: o contrário. A primeira é bem clara e a última é exatamente a cor
   do fundo escuro.

Não comece sem essas três respostas.

## Regras (não quebre nenhuma)

- **Nunca desenhe as letras à mão.** Nada de `stroke`, linhas grossas ou curvas inventadas. O
  contorno sai da fonte, pelo `outline.py` abaixo, e vira um único `<path>` com `fill`.
- **Nunca invente cores.** Use só as rampas que eu te passar, na ordem que eu passar.
- **Nunca escreva as tabelas de cor na mão.** Elas saem do `build_wordmark.py` abaixo.
- **Não mude o modelo do SVG.** O `build_wordmark.py` já tem o arquivo inteiro. Você só edita as
  rampas, os fundos e o arquivo de contorno.
- **O `feFlood` é sempre `#fff`, também no tema escuro.** O fundo escuro sai da ponta da tabela,
  não do flood. Se você trocar o flood pela cor escura, o fundo sai claro.
- **Use estas posições na rampa:** as cores escuras de 0 a uns .55, as de borda espremidas entre
  .6 e .83 e a cor do fundo de .88 até 1. É isso que deixa o escuro dominar e a cor só na borda.
  A primeira cor de borda ocupa só uns .05: é a faixa estreita que desenha o contorno.
  Siga as posições do exemplo e redistribua se a minha rampa tiver outro número de cores.
- **No tema escuro, a cor antes do fundo é mais clara que o fundo.** Se for mais escura, a letra
  vira um buraco e some inteira quando a faixa passa.
- Mantenha `gradientUnits="userSpaceOnUse"` (sem ele, a palavra pisca inteira em vez de ser
  varrida) e `color-interpolation-filters="sRGB"` (sem ele, as cores saem lavadas).

## Como construir

**1. O contorno.** Rode o `outline.py` com a fonte e a palavra. Ele mede a palavra, escala as
letras pra 107 unidades de altura (todos os números do efeito valem pra essa escala) e salva o
contorno. Precisa de `pip install fonttools`.

```
python3 outline.py fonte.ttf PALAVRA > contorno.txt
```

```python
"""Contorno de uma palavra a partir de um arquivo de fonte. Precisa de: pip install fonttools

Uso: python3 outline.py fonte.ttf PALAVRA
Imprime a largura e a altura na primeira linha e o d do path na segunda.
"""
import sys
from fontTools.ttLib import TTFont
from fontTools.pens.svgPathPen import SVGPathPen
from fontTools.pens.transformPen import TransformPen
from fontTools.pens.boundsPen import BoundsPen

font, text = TTFont(sys.argv[1]), sys.argv[2]
glyphs, cmap = font.getGlyphSet(), font.getBestCmap()
HEIGHT, TRACK = 107, -2  # altura das letras e espaço extra entre elas, nas unidades do SVG


def layout(scale):
    x, items = 0, []
    for ch in text:
        name = cmap[ord(ch)]
        items.append((name, x))
        x += glyphs[name].width * scale + TRACK
    pen = BoundsPen(glyphs)
    for name, dx in items:
        glyphs[name].draw(TransformPen(pen, (scale, 0, 0, -scale, dx, 0)))
    return items, pen.bounds


_, (x0, y0, x1, y1) = layout(1)
scale = HEIGHT / (y1 - y0)
items, (x0, y0, x1, y1) = layout(scale)
pen = SVGPathPen(glyphs, ntos=lambda v: ("%.1f" % v).rstrip("0").rstrip("."))
for name, dx in items:
    glyphs[name].draw(TransformPen(pen, (scale, 0, 0, -scale, dx - x0, -y0)))
print(round(x1 - x0, 1), round(y1 - y0, 1))
print(pen.getCommands())
```

**2. Os dois SVGs.** Troque as rampas e os fundos no `build_wordmark.py` pelos meus e rode. Ele
gera `letreiro-claro.svg` e `letreiro-escuro.svg`. Só usa Python 3.

```
python3 build_wordmark.py contorno.txt
```

```python
"""Gera o letreiro claro e o escuro a partir do contorno e de duas rampas. Só Python 3.

Uso: python3 build_wordmark.py contorno.txt
contorno.txt é a saída de outline.py: "LARGURA ALTURA" na primeira linha, o d do path na segunda.
Edite só as rampas e os fundos. Não mude o modelo do SVG.
"""
import math, sys

# O contorno vem da fonte, nunca desenhado à mão: letras com 107 de altura, canto em (0, 0)
size, PATH = open(sys.argv[1]).read().strip().split("\n", 1)
WIDTH, HEIGHT = (float(v) for v in size.split())

# Rampas: (posição de 0 a 1, cor em hex). 0 é o miolo da letra, 1 é o fundo.
# Do 0,88 em diante, sempre a cor exata do fundo.
LIGHT_BG = "#fefefe"
LIGHT = [(0, "#09243f"), (.14, "#0d375f"), (.26, "#12497e"), (.36, "#165b9e"), (.44, "#457cb1"),
         (.5, "#739dc5"), (.55, "#d0deec"), (.6, "#e0b367"), (.65, "#dd6fa3"), (.72, "#e48cb5"),
         (.78, "#eba9c8"), (.83, "#f8e2ed"), (.88, LIGHT_BG), (1, LIGHT_BG)]
DARK_BG = "#292c33"
DARK = [(0, "#ffffff"), (.2, "#fbf0f5"), (.32, "#f1c5da"), (.42, "#e48cb5"), (.5, "#dd6fa3"),
        (.56, "#e0b367"), (.62, "#a2bdd8"), (.68, "#739dc5"), (.74, "#457cb1"), (.79, "#165b9e"),
        (.84, "#12497e"), (.88, DARK_BG), (1, DARK_BG)]

# A faixa: 44 de branco, 121 de transição, 118 de preto, 121 de transição, inclinada 36°, como uma barra invertida.
# Um ciclo leva 4,4 s.
WHITE, RAMP, BLACK, ANGLE, SPEED = 44.07, 121.19, 118.05, 36, 91.93


def channel(stops, i, t):
    for (a, ca), (b, cb) in zip(stops, stops[1:]):
        if a <= t <= b:
            va, vb = (int(c[1 + 2 * i:3 + 2 * i], 16) / 255 for c in (ca, cb))
            return va + (vb - va) * (t - a) / (b - a)


def table(stops, i):
    # 32 pontos, pras faixas estreitas aparecerem; três casas, com uma folguinha: o navegador arredonda pra baixo
    return " ".join("%.3f" % min(1, channel(stops, i, k / 31) + .0015) for k in range(32))


def svg(stops, uid):
    w, h = round(WIDTH + 44), round(HEIGHT + 44)
    p = WHITE + 2 * RAMP + BLACK
    a = math.radians(ANGLE)
    f = lambda x: "%.3f" % (x / p)
    s = p / math.cos(a)  # um período da faixa, medido na horizontal
    return f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="-22 -22 {w} {h}">
<defs>
<linearGradient id="stripe-{uid}" gradientUnits="userSpaceOnUse" x2="{p * math.cos(a):.1f}" y2="{-p * math.sin(a):.1f}" spreadMethod="repeat">
<stop stop-color="#fff"/><stop offset="{f(WHITE)}" stop-color="#fff"/><stop offset="{f(WHITE + RAMP)}" stop-color="#000"/><stop offset="{f(WHITE + RAMP + BLACK)}" stop-color="#000"/><stop offset="1" stop-color="#fff"/>
</linearGradient>
<filter id="material-{uid}" filterUnits="userSpaceOnUse" x="-22" y="-22" width="{w}" height="{h}" color-interpolation-filters="sRGB">
<feGaussianBlur in="SourceAlpha" stdDeviation="13"/>
<feComposite in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feBlend in="SourceGraphic" mode="overlay"/>
<feComponentTransfer><feFuncR type="linear" slope="1.45"/><feFuncG type="linear" slope="1.45"/><feFuncB type="linear" slope="1.45"/></feComponentTransfer>
</filter>
<filter id="color-{uid}" filterUnits="userSpaceOnUse" x="-22" y="-22" width="{w}" height="{h}" color-interpolation-filters="sRGB">
<feGaussianBlur stdDeviation="7.3" result="soft"/>
<feFlood flood-color="#fff"/>
<feComposite in="soft" operator="over" result="flat"/>
<feTurbulence type="fractalNoise" baseFrequency="4"/>
<feColorMatrix values="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1"/>
<feComposite in="flat" operator="arithmetic" k2="1" k3=".14" k4="-.07"/>
<feComponentTransfer>
<feFuncR type="table" tableValues="{table(stops, 0)}"/>
<feFuncG type="table" tableValues="{table(stops, 1)}"/>
<feFuncB type="table" tableValues="{table(stops, 2)}"/>
</feComponentTransfer>
</filter>
<clipPath id="letras-{uid}"><path d="{PATH}"/></clipPath>
</defs>
<g filter="url(#color-{uid})"><g filter="url(#material-{uid})"><g clip-path="url(#letras-{uid})">
<rect x="{-22 - s:.1f}" y="-22" width="{w + s:.1f}" height="{h}" fill="url(#stripe-{uid})">
<animateTransform attributeName="transform" type="translate" from="0 0" to="{s:.1f} 0" dur="{p / SPEED:.2f}s" repeatCount="indefinite"/>
</rect>
</g></g></g>
</svg>
'''


open("letreiro-claro.svg", "w").write(svg(LIGHT, "claro"))
open("letreiro-escuro.svg", "w").write(svg(DARK, "escuro"))
print("ok: letreiro-claro.svg e letreiro-escuro.svg")
```

O que cada parte faz, pra você conferir:

- **Sombra interna** (`material`): desfoca a forma e guarda só a borda de dentro. Com desfoque
  13, cada traço vira um tubo.
- **Faixa** (`stripe`): um degradê branco, preto e branco, inclinado 36° como uma barra `\`, que
  se repete. A faixa anda pra sempre, e essa é a animação inteira. Ela corta a letra na diagonal, e
  a letra some em pedaços e meias-luas. Quem anda não é o degradê: ele fica parado num retângulo, e
  o retângulo desliza por trás, recortado pelo contorno das letras (`clipPath`). Não anime o degradê.
  No Safari e no iPhone, animar `gradientTransform` deixa a faixa parada, e animar `x1` e `x2`
  juntos estica a faixa em vez de deslizar.
- **Cor** (`color`): desfoque de 7,3, fundo branco, grão e a tabela que troca cada cinza pela cor
  da rampa. O desfoque borra a borda da letra, e é ela que ganha o anel de cores.

**3. Movimento reduzido.** Gere um quadro parado de cada versão, em WebP, pausando a animação
(`svg.pauseAnimations()` e `svg.setCurrentTime(3.4)`) e fotografando. Na página:

```html
<picture>
  <source media="(prefers-reduced-motion: reduce)" srcset="letreiro-claro-parado.webp">
  <img src="letreiro-claro.svg" alt="…">
</picture>
```

## Como conferir antes de me entregar

Renderize quadros em 0, 1, 2, 3, 4 e 5 segundos, nos dois temas, e me mostre. Compare com os
arquivos de gabarito. Confira:

- as letras são as da fonte, com cantos e curvas de verdade, sem cara de traço de caneta;
- a faixa varre a palavra de um lado pro outro, também no Safari, e não pisca;
- o miolo é escuro e as cores aparecem só na borda e onde a faixa passa;
- as cores são as da minha rampa, na ordem, sem arco-íris;
- o fundo é a cor do meu fundo, sem retângulo em volta da palavra;
- no escuro, a letra nunca vira um buraco mais escuro que o fundo;
- cada arquivo tem uns 3 ou 4 KB.
