pro-visu docs
Generators

specimen

A looping type-specimen video from a font file.

For AI / LLMs: View Markdown

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.

{
  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.

fontstringrequired

Font file to showcase (path relative to the working dir, or absolute). 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.

namestringdefault ''

Display name shown in the bottom gap area (e.g. "ABC Oracle"). Position/style it via label.

outputobjectdefault {}

Output frame + encoding settings. Override any subset.

widthnumberdefault 1920

Output frame width in px.

heightnumberdefault 1080

Output frame height in px.

deviceScaleFactornumber (≤4)default 1

Render scale (1 = 1:1; higher = crisper capture, downscaled into the video).

fpsnumber (≤120)default 30

Output frames per second.

crfnumber (0–51)default 18

x264 quality (lower = better quality / larger file).

fileNamestring

Output filename. Defaults to "<slug(asset name)>.mp4".

typeobjectdefault {}

Glyph typography — the type set on the wall. Override any subset.

weightnumber (100–1000, step 100)default 400

Glyph weight, in steps of 100 to match the weights a font actually ships. 400 = regular.

linesnumber (1–40)default 3

Number of glyph rows. The glyph size is derived so the rows fill "fill" of the frame height.

fillnumber (0–1)default 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.

leadingnumberdefault 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.

padding'xs' | 'sm' | 'base' | 'lg' | 'xl' | percentage string | number (px)default '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.

opticalMarginsbooleandefault 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.

blackliststringdefault ''

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.

characterPoolstring

Override the glyph pool (needs ≥2 distinct characters). Defaults to A–Z, 0–9, and the symbols $ % & @ # * + =. Digits always render as lining figures.

widthTolerancenumber (0–0.5)default 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.

labelobjectdefault {}

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).

anchor'top-left' | 'top-center' | 'top-right' | 'middle-left' | 'middle-center' | 'middle-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'default 'bottom-left'

Corner/edge of the gap the label anchors to.

padding'xs' | 'sm' | 'base' | 'lg' | 'xl' | percentage string | number (px)default '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.

size'xs' | 'sm' | 'base' | 'lg' | 'xl' | number (0–1)default '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.

weightnumber (1–1000)default 500

Label font weight.

colorstring

Label text colour (any CSS colour). Falls back to colors.foreground if unset.

colorsobjectdefault light-grey palette

Colour tokens the glyphs cycle through (any CSS colour strings). Override any subset; each key falls back independently.

backgroundstringdefault '#eceef1'

Backdrop behind the glyphs (and the bottom gap band).

foregroundstringdefault '#16181d'

Primary glyph colour — the resting majority.

mutedstringdefault '#a7adb6'

Muted/secondary glyph colour.

accentstring

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.

colorWeightsobjectdefault 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).

foregroundnumber (≥0)default 2

Relative likelihood of the foreground token on a random colour change.

mutednumber (≥0)default 2

Relative likelihood of the muted token on a random colour change.

accentnumber (≥0)default 1

Relative likelihood of the accent token on a random colour change.

pulsesPulse[]default 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).

namestringdefault ''

Human label for the beat (readability only).

durationMsnumberrequired

Length of the beat, in milliseconds. Required, must be positive.

charsnumber (≥0)default 0

Fraction of cells whose glyph changes during the beat (1 = every cell once; above 1 repeats cells; 0 = a hold).

colorsnumber (≥0)default 0

Fraction of cells whose colour changes during the beat (1 = every cell once; above 1 repeats cells).

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.

pacing'even' | 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'random'default '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.

animationobjectdefault {}

Animation timing + behaviour. Override any subset.

durationMsnumber

Clip length in ms. Defaults to the (mirrored) sum of the pulse durations; set to override.

mirrorbooleandefault 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.

seednumberdefault 1

Schedule seed — same seed ⇒ identical animation. Change for a different (still deterministic) take.

characterIntensitynumber (≥0)default 1

Multiply every pulse's glyph-change fraction (1 = baseline, 2 = twice as busy, 0 = none).

colorIntensitynumber (≥0)default 1

Multiply every pulse's colour-change fraction (1 = baseline, 2 = twice as busy, 0 = none).

demobooleandefault false

Overlay the active pulse's name bottom-right, to see which beat is playing.

{
  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.

{name: "type-sans",generator: "specimen",options: {  font: "public/fonts/Inter.woff2",  name: "Inter",  template: "sweep",  type: { lines: 4 },},}
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.

{name: "type-demo",generator: "specimen",options: {  font: "public/fonts/Fraunces.woff2",  name: "Fraunces",  template: "demo",},}
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.

{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" },  ],},}
9:16 numeric ticker: 12 lines at fill 0.94 fill the height, and mono digits render as an even grid.
{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" },},}
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.

{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},}
Weight 100: few lines, airy leading and calm motion make a spacious, elegant wall.
{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},}
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.

{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},}
Accent-led: the accent is 4× as likely and colour runs hot while the glyphs stay calm.
{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 },},}
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.

{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" },},}
Lowercase text face: a–z brings descenders, so leading rises to 1.0 to keep g j p q y from clipping.
{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" },},}
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.

{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 },  ],},}
mirror: false — the clip assembles from near-blank and holds composed instead of looping back.

On this page