# Prompt: animated gradient wordmark in a ~3 KB SVG

Paste this into your agent (Claude Code, Cursor, whatever you use).

## What I want

An animated wordmark in a single SVG file of about 3 KB, with no JavaScript, no canvas and no
WebGL. A stripe of light sweeps the word forever, and every shade of grey becomes a color from
my brand. The core of the letters stays dark and the color shows at the edge and where the
stripe passes. I want one version for the light theme and one for the dark.

The technique is by Mike Bespalov: https://x.com/bbssppllvv/status/2104747296671883312
This prompt reproduces the version Gabriel Souza made for PDR, and his files work as a
reference: https://gsouza.xyz/writing/pdr-gradient/pdr-claro.svg and
https://gsouza.xyz/writing/pdr-gradient/pdr-escuro.svg

## Before you start, ask me

1. **The word** and **the font file** (.ttf or .otf). Prefer a heavy weight (Bold, Black): the
   effect happens inside the stroke, and a thin stroke can't hold it.
2. **The light ramp**: 8 to 16 hex colors, from the core of the letter to its edge. The first
   is very dark and the last is exactly the color of the background the wordmark sits on.
3. **The dark ramp**: the opposite. The first is very light and the last is exactly the dark
   background color.

Don't start without these three answers.

## Rules (break none of them)

- **Never draw the letters by hand.** No `stroke`, thick lines or made-up curves. The outline
  comes from the font, through `outline.py` below, and becomes a single `<path>` with a `fill`.
- **Never make up colors.** Use only the ramps I give you, in the order I give them.
- **Never write the color tables by hand.** They come out of `build_wordmark.py` below.
- **Don't change the SVG template.** `build_wordmark.py` already has the whole file. You only
  edit the ramps, the backgrounds and the outline file.
- **The `feFlood` is always `#fff`, in the dark theme too.** The dark background comes from the
  end of the table, not from the flood. If you set the flood to the dark color, the background
  comes out light.
- **Use these ramp positions:** the dark colors from 0 to about .55, the edge colors squeezed
  between .6 and .83 and the background color from .88 to 1. That's what lets the dark rule and
  keeps the color at the edge. The first edge color takes only about .05: the narrow band is what
  draws the outline. Follow the example's positions and spread them out
  again if my ramp has a different number of colors.
- **In the dark theme, the color before the background is lighter than the background.** If it's
  darker, the letter turns into a hole and vanishes whole when the stripe goes by.
- Keep `gradientUnits="userSpaceOnUse"` (without it, the whole word blinks instead of being
  swept) and `color-interpolation-filters="sRGB"` (without it, the colors come out washed out).

## How to build it

**1. The outline.** Run `outline.py` with the font and the word. It measures the word, scales
the letters to 107 units tall (every number in the effect is for that scale) and saves the
outline. Needs `pip install fonttools`.

```
python3 outline.py font.ttf WORD > outline.txt
```

```python
"""Outline of a word from a font file. Needs: pip install fonttools

Usage: python3 outline.py font.ttf WORD
Prints the width and height on the first line and the path's d on the second.
"""
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  # letter height and extra space between letters, in SVG units


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. The two SVGs.** Replace the ramps and backgrounds in `build_wordmark.py` with mine and run
it. It outputs `wordmark-light.svg` and `wordmark-dark.svg`. Python 3 only.

```
python3 build_wordmark.py outline.txt
```

```python
"""Builds the light and dark wordmark from the outline and two ramps. Python 3 only.

Usage: python3 build_wordmark.py outline.txt
outline.txt is the output of outline.py: "WIDTH HEIGHT" on the first line, the path's d on the second.
Edit only the ramps and backgrounds. Don't change the SVG template.
"""
import math, sys


# The outline comes from the font, never drawn by hand: letters 107 tall, corner at (0, 0)
size, PATH = open(sys.argv[1]).read().strip().split("\n", 1)
WIDTH, HEIGHT = (float(v) for v in size.split())

# Ramps: (position from 0 to 1, hex color). 0 is the core of the letter, 1 is the background.
# From 0.88 on, always the exact background color.
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)]

# The stripe: 44 of white, 121 of transition, 118 of black, 121 of transition, tilted 36°, like a backslash.
# One cycle takes 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 points, so the narrow bands show up; three decimals, plus a tiny margin: the browser rounds down
    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)  # one period of the stripe, measured horizontally
    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="letters-{uid}"><path d="{PATH}"/></clipPath>
</defs>
<g filter="url(#color-{uid})"><g filter="url(#material-{uid})"><g clip-path="url(#letters-{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("wordmark-light.svg", "w").write(svg(LIGHT, "light"))
open("wordmark-dark.svg", "w").write(svg(DARK, "dark"))
print("ok: wordmark-light.svg and wordmark-dark.svg")
```

What each part does, so you can check:

- **Inner shadow** (`material`): blurs the shape and keeps only the inside edge. With a blur of
  13, each stroke turns into a tube.
- **Stripe** (`stripe`): a white, black and white gradient, tilted 36° like a `\` bar, that
  repeats. The stripe moves forever, and that's the whole animation. It cuts the letter diagonally,
  and the letter disappears in pieces and half-moons. The gradient itself doesn't move: it sits
  still on a rectangle, and the rectangle slides behind, clipped to the letters' outline (`clipPath`).
  Don't animate the gradient. In Safari and on the iPhone, animating `gradientTransform` leaves the
  stripe standing still, and animating `x1` and `x2` together stretches it instead of sliding it.
- **Color** (`color`): a 7.3 blur, a white background, grain and the table that swaps each grey
  for the ramp's color. The blur smears the edge of the letter, and that edge gets the ring of
  colors.

**3. Reduced motion.** Make a still frame of each version, as WebP, by pausing the animation
(`svg.pauseAnimations()` and `svg.setCurrentTime(3.4)`) and taking a screenshot. On the page:

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

## How to check before handing it over

Render frames at 0, 1, 2, 3, 4 and 5 seconds, in both themes, and show me. Compare them with the
reference files. Check that:

- the letters are the font's, with real corners and curves, nothing that looks like a marker;
- the stripe sweeps the word from one side to the other, in Safari too, and doesn't blink;
- the core is dark and the colors show only at the edge and where the stripe passes;
- the colors are my ramp's, in order, no rainbow;
- the background is my background color, with no rectangle around the word;
- in the dark theme, the letter never turns into a hole darker than the background;
- each file is about 3 or 4 KB.
