# specimen

URL: /docs/generators/specimen

A looping type-specimen video from a font file.

`specimen` is a **local** generator — no `url`. Point it at a font file and it renders a looping
clip of that typeface set as a fixed number of **left-aligned lines** of glyphs, whose letters and
colours change over a composed sequence of **pulses**, then captures it. The glyph size is derived
from `lines` (the rows fill the top `type.fill` of the frame — 80% by default); the strip below is a
gap that shows the background and the font `name`.

```ts
{
  name: "brand-type",
  generator: "specimen",
  options: {
    font: "public/fonts/YourFont.woff2",
    name: "Your Font",
    template: "sweep",
  },
}
```

Only `font` is required; everything else has a sensible default. A **pulse** is one beat of the
animation (a named span during which some fraction of the glyph cells change letter and/or colour;
an empty pulse is a hold), and the clip length is the sum of the pulse durations — doubled when
`mirror` is on, which plays the storyboard out and back for a seamless loop. Omit `pulses` and a
lively built-in storyboard is used. A `template` (`"sweep"` or `"demo"`) loads a named preset, and
your explicit options still override what it sets.

## Config options

Only `font` is required. **Reference** is the interactive view; **TypeScript** is the same shape
in code, every option at its default.

**Reference**

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `font` | `string` |  | Font file to showcase (path relative to the working dir, or absolute). Required. _(required)_ |
| `template` | `'demo' \| 'sweep'` |  | Load a named option preset; your explicit options still override what it sets (deep, per-field). "sweep" is a seamless-looping showcase of even per-character colour sweeps (muted → accent → foreground) on a dark palette chosen so the accent reads. "demo" is a labelled walkthrough of every pulse behaviour with demo mode on; runs once, no mirror. |
| `name` | `string` | `''` | Display name shown in the bottom gap area (e.g. "ABC Oracle"). Position/style it via label. |
| `output` | `object` | `{}` | Output frame + encoding settings. Override any subset. |
| `output.width` | `number` | `1920` | Output frame width in px. |
| `output.height` | `number` | `1080` | Output frame height in px. |
| `output.deviceScaleFactor` | `number (≤4)` | `1` | Render scale (1 = 1:1; higher = crisper capture, downscaled into the video). |
| `output.fps` | `number (≤120)` | `30` | Output frames per second. |
| `output.crf` | `number (0–51)` | `18` | x264 quality (lower = better quality / larger file). |
| `output.fileName` | `string` |  | Output filename. Defaults to "<slug(asset name)>.mp4". |
| `type` | `object` | `{}` | Glyph typography — the type set on the wall. Override any subset. |
| `type.weight` | `number (100–1000, step 100)` | `400` | Glyph weight, in steps of 100 to match the weights a font actually ships. 400 = regular. |
| `type.lines` | `number (1–40)` | `3` | Number of glyph rows. The glyph size is derived so the rows fill "fill" of the frame height. |
| `type.fill` | `number (0–1)` | `0.8` | Fraction of the frame height the glyph rows fill; the strip below is the label gap area. Raise toward 1 for a fuller wall, lower it to make more room for the name label. |
| `type.leading` | `number` | `0.78` | Line-height of the rows (tight, cap-height-hugging). The tight default clips below the baseline, so lowercase descenders (g j p q y) in a custom pool get cut off — raise to ~1 if you include them. |
| `type.padding` | `'xs' \| 'sm' \| 'base' \| 'lg' \| 'xl' \| percentage string \| number (px)` | `'base'` | Side margin the glyph wall is inset by, left and right — a step of the size scale, a percentage of the frame height like "3%", or px as a number. "base" is 32px at 1080p. Steps and percentages keep the margin in proportion to the glyphs, which are sized from the frame height, so the framing holds at any output size; px pins it. Matches label.padding by default, so the wall's edges line up with the name label below it. Lines justify flush to the remaining content width, so this is a margin you can rely on. Set 0 for an edge-to-edge wall that bleeds off the frame. |
| `type.opticalMargins` | `boolean` | `true` | Flush each line's ink to the margins rather than its metrics. Advance widths include a little blank side bearing per glyph, so a metrically-aligned line still reads as ragged against the frame edge. This slides just the first and last glyph inside their fixed boxes to cancel it. Neighbours are unaffected and no glyph is frozen. Set false to align by metrics. |
| `type.blacklist` | `string` | `''` | Glyphs to exclude from the pool, case-insensitively (e.g. "qxz" and "QXZ" both drop Q, X, Z). A blacklist that would empty the pool is ignored. |
| `type.characterPool` | `string` |  | Override the glyph pool (needs ≥2 distinct characters). Defaults to A–Z, 0–9, and the symbols $ % & @ # * + =. Digits always render as lining figures. |
| `type.widthTolerance` | `number (0–0.5)` | `0.02` | How close in width two glyphs must be to substitute for each other, as a fraction of advance. Every cell is a fixed-width box and only ever holds glyphs within this tolerance of the one it was seeded with, so a glyph change moves nothing at all. Raise it for more glyphs per cell (and slightly looser, less even letter gaps); lower it for tighter spacing (and fewer glyphs able to change). Glyphs left with no width-compatible partner still appear but never change — the run logs which ones. |
| `label` | `object` | `{}` | Placement + styling of the name label within the bottom gap area. Position is confined to the gap (the label moves around it, never over the glyphs). |
| `label.anchor` | `'top-left' \| 'top-center' \| 'top-right' \| 'middle-left' \| 'middle-center' \| 'middle-right' \| 'bottom-left' \| 'bottom-center' \| 'bottom-right'` | `'bottom-left'` | Corner/edge of the gap the label anchors to. |
| `label.padding` | `'xs' \| 'sm' \| 'base' \| 'lg' \| 'xl' \| percentage string \| number (px)` | `'base'` | Inset from the gap edges, all around, measured to the visible text — a step of the size scale, a percentage of the frame height like "3%", or px as a number. "base" is 32px at 1080p. Steps and percentages keep the inset in proportion to the label text, which is sized from the frame height, so the corner reads the same at any output size; px pins it. 0 = flush to the rendered corner. |
| `label.size` | `'xs' \| 'sm' \| 'base' \| 'lg' \| 'xl' \| number (0–1)` | `'base'` | Text size — a step of the size scale, or a fraction of the gap-area height. "base" is 0.22. Either way it scales with the frame height. |
| `label.weight` | `number (1–1000)` | `500` | Label font weight. |
| `label.color` | `string` |  | Label text colour (any CSS colour). Falls back to colors.foreground if unset. |
| `colors` | `object` | `light-grey palette` | Colour tokens the glyphs cycle through (any CSS colour strings). Override any subset; each key falls back independently. |
| `colors.background` | `string` | `'#eceef1'` | Backdrop behind the glyphs (and the bottom gap band). |
| `colors.foreground` | `string` | `'#16181d'` | Primary glyph colour — the resting majority. |
| `colors.muted` | `string` | `'#a7adb6'` | Muted/secondary glyph colour. |
| `colors.accent` | `string` |  | Accent colour for occasional pops. Falls back to background if unset (accent glyphs blend into the backdrop) — set it (or a template) to make accent reads visible. |
| `colorWeights` | `object` | `2 / 2 / 1` | Relative likelihood of each token on a random (non-targeted) recolour. A weight of 0 excludes that token from random recolouring (an explicit pulse color can still target it). |
| `colorWeights.foreground` | `number (≥0)` | `2` | Relative likelihood of the foreground token on a random colour change. |
| `colorWeights.muted` | `number (≥0)` | `2` | Relative likelihood of the muted token on a random colour change. |
| `colorWeights.accent` | `number (≥0)` | `1` | Relative likelihood of the accent token on a random colour change. |
| `pulses` | `Pulse[]` | `built-in storyboard` | The animation storyboard: an ordered sequence of beats (min 1). Omit for the built-in default (a lively ~10s outward half, mirrored into a ~20s loop). The clip length is the sum of the pulse durations (doubled when mirror is on). |
| `pulses.name` | `string` | `''` | Human label for the beat (readability only). |
| `pulses.durationMs` | `number` |  | Length of the beat, in milliseconds. Required, must be positive. _(required)_ |
| `pulses.chars` | `number (≥0)` | `0` | Fraction of cells whose glyph changes during the beat (1 = every cell once; above 1 repeats cells; 0 = a hold). |
| `pulses.colors` | `number (≥0)` | `0` | Fraction of cells whose colour changes during the beat (1 = every cell once; above 1 repeats cells). |
| `pulses.color` | `'foreground' \| 'muted' \| 'accent'` |  | Target every colour change at this token (a deliberate sweep) instead of a weighted-random pick. Set colors: 1 with pacing: "even" to wash the whole specimen to one colour evenly. Omit for the default scattered recolour. |
| `pulses.pacing` | `'even' \| 'linear' \| 'ease-in' \| 'ease-out' \| 'ease-in-out' \| 'random'` | `'even'` | How changes are distributed in time across the beat. even/linear are uniform; ease-in front-loads; ease-out back-loads; ease-in-out bunches at both ends; random scatters. |
| `animation` | `object` | `{}` | Animation timing + behaviour. Override any subset. |
| `animation.durationMs` | `number` |  | Clip length in ms. Defaults to the (mirrored) sum of the pulse durations; set to override. |
| `animation.mirror` | `boolean` | `true` | Play the pulses out and back as a palindrome for a seamless loop; doubles the clip length. Set false for a one-shot that ends on the last state. |
| `animation.seed` | `number` | `1` | Schedule seed — same seed ⇒ identical animation. Change for a different (still deterministic) take. |
| `animation.characterIntensity` | `number (≥0)` | `1` | Multiply every pulse's glyph-change fraction (1 = baseline, 2 = twice as busy, 0 = none). |
| `animation.colorIntensity` | `number (≥0)` | `1` | Multiply every pulse's colour-change fraction (1 = baseline, 2 = twice as busy, 0 = none). |
| `animation.demo` | `boolean` | `false` | Overlay the active pulse's name bottom-right, to see which beat is playing. |

**TypeScript**

```ts
{
  name: "brand-type",
  generator: "specimen",
  options: {
    // --- source & preset (specimen is local: no url) ---
    font: "public/fonts/YourFont.woff2",
    // template: "sweep",              // optional; "sweep" | "demo" preset (your options still win)

    // --- name label (lives in the bottom gap) ---
    name: "",
    label: {
      anchor: "bottom-left",
      padding: 32,
      size: 0.22,
      weight: 500,
      // color: "#b49a77",             // optional; falls back to colors.foreground
    },

    // --- type & layout ---
    type: {
      weight: 400,                    // steps of 100 (100–1000)
      lines: 3,
      fill: 0.8,                      // fraction of the frame the rows fill
      leading: 0.78,
      padding: 32,                    // side margin the wall justifies within (0 = edge-to-edge)
      opticalMargins: true,           // flush the ink to the margin, not the metrics
      blacklist: "",
      widthTolerance: 0.02,           // ±2% — how close in width a glyph swap must be
      // characterPool: "ABC…",       // optional; defaults to A–Z 0–9 + symbols ($ % & @ # * + =)
    },

    // --- colour ---
    colors: {
      background: "#eceef1",
      foreground: "#16181d",
      muted: "#a7adb6",
      // accent: "#7c9cff",            // optional; falls back to background (accent blends in)
    },
    colorWeights: {
      foreground: 2,
      muted: 2,
      accent: 1,
    },

    // --- storyboard (omit pulses for the built-in default) ---
    // pulses: [{ name: "hold", durationMs: 800 }, { name: "to accent", durationMs: 2200, colors: 1, color: "accent", pacing: "ease-in-out" }],

    // --- animation (timing, loop & determinism) ---
    animation: {
      mirror: true,
      seed: 1,
      characterIntensity: 1,
      colorIntensity: 1,
      demo: false,
      // durationMs: 20000,           // optional; defaults to the (mirrored) sum of pulse durations
    },

    // --- output & encoding ---
    output: {
      width: 1920,
      height: 1080,
      deviceScaleFactor: 1,
      fps: 30,
      crf: 18,
      // fileName: "brand-type.mp4",  // optional; defaults to <slug(name)>.mp4
    },
  },
}
```

> `seed` makes the otherwise-scattered animation deterministic: the same seed always produces the
> same take. Change it for a different (still reproducible) result. Every option also has hover docs
> in `pro-visu.config.ts` — the authoring types are generated from the validation schema.

A specimen is sized off the frame **height**: the glyphs are derived from it, the label text is a
fraction of the gap area beneath them, and the two `padding` options resolve against it too. So the
whole composition holds its proportions at any output size — set `output` to 4K and everything grows
with it, no other options to touch.

Both paddings and `label.size` take a step of the size scale — `xs`, `sm`, `base`, `lg`, `xl` —
which is the easiest way to say "a bit tighter" or "a bit bigger" without picking a number. `base`
is the shipped default in each case. Reach past the scale when you need to: a percentage of the
frame height (`"4.5%"`) or a fraction for `label.size` (`0.16`) is exact and still scales, and a
plain number of px pins a padding at that size whatever the output.

## Examples

Start from a `template` for an instant look, or compose the options into your own. The settings
reinforce one another, so a handful of decisions tend to travel together — each recipe below
foregrounds one such decision and lets the rest fall in behind it.

### Templates

A `template` loads a named preset; your explicit options still override what it sets.

`template: "sweep"` is a seamless-looping showcase of even per-character colour sweeps
(muted → accent → foreground) on a dark palette chosen so the accent reads.

```ts
{
name: "type-sans",
generator: "specimen",
options: {
  font: "public/fonts/Inter.woff2",
  name: "Inter",
  template: "sweep",
  type: { lines: 4 },
},
}
```

*Output: The sweep template on Inter — a seamless looping wash of per-glyph colour sweeps.*

`template: "demo"` walks through every pulse behaviour one beat at a time with demo mode on, so it
overlays each beat's name as it plays — you can see exactly what every pulse setting does.

```ts
{
name: "type-demo",
generator: "specimen",
options: {
  font: "public/fonts/Fraunces.woff2",
  name: "Fraunces",
  template: "demo",
},
}
```

*Output: The demo template on Fraunces — a labelled tour through linear / ease-in / ease-out / colour sweeps.*

### Aspect ratio

The frame's shape decides how the type is set. A tall **9:16** frame has vertical room for many
`lines` at a high `fill`; a wide letterbox wants a single big line. Tabular digits in a monospaced
face lock to a flawless grid.

```ts
{
name: "numeric-ticker",
generator: "specimen",
options: {
  font: "public/fonts/JetBrainsMono.woff2",
  name: "0–9",
  output: { width: 1080, height: 1920 },   // 9:16 — vertical room for the rows
  type: {
    lines: 12,                              // many rows…
    fill: 0.94,                             // …want a high fill to use the height
    characterPool: "0123456789",            // mono digits ⇒ a flawless grid
    weight: 500,
  },
  colors: { background: "#0b0b0f", foreground: "#e8e8ea", muted: "#4b5563", accent: "#6ee7a8" },
  label: { anchor: "bottom-center", size: 0.16 },
  pulses: [
    { name: "flip", durationMs: 1400, chars: 0.6, pacing: "random" },
    { name: "accent", durationMs: 800, colors: 0.2, color: "accent" },
  ],
},
}
```

*Output: 9:16 numeric ticker: 12 lines at fill 0.94 fill the height, and mono digits render as an even grid.*

```ts
{
name: "wordmark-banner",
generator: "specimen",
options: {
  font: "public/fonts/Inter.woff2",
  name: "Inter",
  output: { width: 2560, height: 640 },        // 4:1 letterbox
  type: { lines: 1, fill: 0.72, weight: 800 }, // one fat line suits the strip
  label: { anchor: "bottom-right" },
},
}
```

*Output: 4:1 ultrawide banner: a single heavy line is the only thing that suits the letterbox strip.*

### Weight & density

Glyph `weight` reads best at a matching density. A hairline only holds up **large** — few lines,
airy `leading`, calm motion. A black weight wants the opposite: pack the rows tight and let it churn.

```ts
{
name: "hairline",
generator: "specimen",
options: {
  font: "public/fonts/Inter.woff2",
  name: "Inter Thin",
  type: { weight: 100, lines: 4, leading: 1.15 },              // thin + airy = spacious
  label: { anchor: "bottom-left", weight: 300 },
  animation: { characterIntensity: 0.4, colorIntensity: 0.3 }, // calm, to match
},
}
```

*Output: Weight 100: few lines, airy leading and calm motion make a spacious, elegant wall.*

```ts
{
name: "heavy",
generator: "specimen",
options: {
  font: "public/fonts/Inter.woff2",
  name: "Inter Black",
  type: { weight: 900, lines: 10, leading: 0.72 },             // black wants dense, tight rows
  colors: { background: "#0b0b0f", foreground: "#f4f4f5", accent: "#7c9cff" },
  label: { anchor: "bottom-right" },
  animation: { characterIntensity: 1.8, colorIntensity: 1.3 }, // busy churn
},
}
```

*Output: Weight 900: ten tight lines and a busy churn on a dark palette — loud and kinetic.*

### Colour

`colorWeights` and `colorIntensity` decide how much colour carries the piece. Bias the accent and run
colour hot — while keeping the glyphs calm — and colour leads. Omit the accent (it falls back to the
background, so pops vanish) and let muted dominate for a restrained, single-hue wall.

```ts
{
name: "accent-led",
generator: "specimen",
options: {
  font: "public/fonts/Inter.woff2",
  name: "accent-led",
  colors: { background: "#07070a", foreground: "#e8e8ea", muted: "#2a2a33", accent: "#6ee7a8" },
  colorWeights: { foreground: 1, muted: 1, accent: 4 },        // accent 4× as likely
  label: { anchor: "bottom-left", color: "#6ee7a8" },
  animation: { characterIntensity: 0.5, colorIntensity: 1.8 }, // colour moves, glyphs rest
},
}
```

*Output: Accent-led: the accent is 4× as likely and colour runs hot while the glyphs stay calm.*

```ts
{
name: "monochrome",
generator: "specimen",
options: {
  font: "public/fonts/Inter.woff2",
  name: "monochrome",
  colors: { background: "#f6f3ed", foreground: "#1a1714", muted: "#b49a77" }, // no accent ⇒ = background
  colorWeights: { foreground: 1, muted: 3, accent: 0 },        // pops disabled; muted leads
  label: { anchor: "bottom-left", color: "#b49a77" },
  animation: { characterIntensity: 0.4, colorIntensity: 0.5 },
},
}
```

*Output: Monochrome: accent omitted (so pops blend into the background) and muted dominates — restrained.*

### Glyph pool

Swap `characterPool` and the specimen changes character — but the pool couples to `leading`. A
lowercase pool has descenders (g j p q y) that the tight default clips, so raise `leading` toward 1.
A symbol pool locks to an even grid in a monospaced face.

```ts
{
name: "text-face",
generator: "specimen",
options: {
  font: "public/fonts/Fraunces.woff2",
  name: "Fraunces text",
  type: {
    characterPool: "abcdefghijklmnopqrstuvwxyz", // lowercase ⇒ descenders
    leading: 1.0,                                // raised so g j p q y don't clip
    lines: 4,
  },
  colors: { background: "#f6f3ed", foreground: "#1a1714", muted: "#b49a77", accent: "#8a5a3c" },
  label: { anchor: "bottom-left", color: "#8a5a3c" },
},
}
```

*Output: Lowercase text face: a–z brings descenders, so leading rises to 1.0 to keep g j p q y from clipping.*

```ts
{
name: "symbols",
generator: "specimen",
options: {
  font: "public/fonts/JetBrainsMono.woff2",
  name: "JetBrains Mono · symbols",
  type: {
    characterPool: "!@#$%^&*()_+-=[]{};:,.<>/?", // punctuation set
    lines: 6,                                     // mono ⇒ even grid
  },
  colors: { background: "#0b0f10", foreground: "#cdd6d3", muted: "#586460", accent: "#6ee7a8" },
  label: { anchor: "bottom-right" },
},
}
```

*Output: Punctuation pool in a monospaced face — the odd-shaped glyphs still lock to an even grid.*

### Motion

`mirror` (on by default) plays the storyboard out and back for a seamless loop. Turn it **off** for a
one-shot that ends on its final state — write the `pulses` to assemble from near-blank and hold, like
an intro sting.

```ts
{
name: "one-shot",
generator: "specimen",
options: {
  font: "public/fonts/Inter.woff2",
  name: "one-shot",
  type: { lines: 3 },
  label: { anchor: "bottom-left" },
  animation: { mirror: false },   // ends on the last state instead of looping back
  pulses: [
    { name: "sparse", durationMs: 600 },
    { name: "assemble", durationMs: 2200, chars: 0.9, pacing: "ease-out" },
    { name: "colour in", durationMs: 1600, colors: 0.7, pacing: "ease-in-out" },
    { name: "hold final", durationMs: 1600 },
  ],
},
}
```

*Output: mirror: false — the clip assembles from near-blank and holds composed instead of looping back.*
