No description
  • Python 55.7%
  • CSS 20%
  • HTML 17.3%
  • JavaScript 7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-26 22:23:18 +01:00
docs Release TT2026 1.0.1 with revised weights and named desktop styles 2026-10-26 21:49:08 +01:00
experiments Rebalance TT2026 weights and refresh all regular web fonts 2026-10-26 19:00:08 +01:00
presets Expand TT2026 into verified on-demand font packages 2026-10-25 09:37:47 +01:00
releases Release TT2026 1.0.1 with revised weights and named desktop styles 2026-10-26 21:49:08 +01:00
src/tt2026 Release TT2026 1.0.1 with revised weights and named desktop styles 2026-10-26 21:49:08 +01:00
tests Release TT2026 1.0.1 with revised weights and named desktop styles 2026-10-26 21:49:08 +01:00
tools Release TT2026 1.0.1 with revised weights and named desktop styles 2026-10-26 21:49:08 +01:00
website Vary site heading and paragraph impressions by sibling position 2026-10-26 22:23:18 +01:00
.gitignore Keep release archives outside the website and link the live specimen 2026-10-25 23:01:19 +01:00
LICENSE Clarify copyright notice for TT2026 modifications 2026-10-24 14:37:21 +02:00
pyproject.toml Release TT2026 1.0.1 with revised weights and named desktop styles 2026-10-26 21:49:08 +01:00
README.md Lead starting-impression docs with headings and sibling-index 2026-10-26 22:18:54 +01:00
tt2026.toml Rebalance TT2026 weights and refresh all regular web fonts 2026-10-26 19:00:08 +01:00
uv.lock Release TT2026 1.0.1 with revised weights and named desktop styles 2026-10-26 21:49:08 +01:00

TT2026

A configurable typewriter font workshop, continuing Fredrick Brennan’s TT2020.

Ten weights, three levels of ribbon wear, upright and italic, and ten stored impressions of each character. Broad missing-ink patches, pooling in corners, uneven key pressure and curved outlines give each strike a little individuality. The font handles alternation itself through OpenType; no JavaScript text replacement is required.

Explore the fonts, try the interactive specimens and download the release at tt2026.hex9d.com.

This is a new project and build interface, not a compatibility layer for TT2020.

Version 1.0.1 uses a revised weight range: 400 sits approximately midway between the original 400 and 500, with a larger increase at the light end tapering to zero at 1000. Both styles of regular TT2026 and LRT use the revised base weights. Weight comparison and measurements.

Download

Desktop TTFs and TTCs use Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black and Extra Black, corresponding to 100–1000, with matching italic styles. Numeric weights and CSS stay unchanged.

Version 1.0.1 has separate web, TTF and TTC ZIPs for TT2026 and The Last Remaining Typewriter. Choose web for a website, individual TTFs for desktop installation, or a TTC collection for fewer installed files. Each TTC contains the same faces as its TTF bundle. Release notes describe coverage and features.

The files page links to the release’s ready-made desktop TTFs, TTC collections and web WOFF2 bundles for TT2026 and The Last Remaining Typewriter, including built-in x, dash and underscore corrections. No build tools are needed to use them. Every ZIP includes the license and a short usage guide. Release ZIPs are hosted on Forgejo; the website links directly to them.

Try it

Install uv, then:

git clone https://git.hex9d.com/kludge/tt2026.git
cd tt2026
uv sync
uv run tt2026 build --config presets/quick.toml --no-web-output

The quick preset builds weight 400, the Worn wear level, both styles and three impressions. For an optional browser preview, run python3 website/preview.py --fonts build and open http://127.0.0.1:8765/. The static specimen reads that font folder directly; no website build or export is needed. See website setup for local preview and static hosting.

Python 3.13+ is required. Source outlines are bundled; you do not need another repository, FontForge, GIMP, Node or a frontend framework. The locked environment was tested on macOS with Python 3.15. Other platforms may need a compiler if a dependency has no wheel for the selected Python version.

Build the full family

uv run tt2026 build
uv run tt2026 check
uv run tt2026 collection

The default tt2026.toml builds 60 static faces: weights 100–1000 × subtle/worn/battered wear × upright/italic. Each face has ten impressions for each of the current 114 characters.

  • build/*.ttf: individual desktop fonts, containing the complete requested character set.
  • build/*.woff2 and build/fonts.css: web fonts with explicit Unicode ranges.
  • build/TT2026.ttc: optional single-file desktop collection of every built face.
  • build/manifest.json: configuration, character coverage, file sizes and checksums.
  • .cache/tt2026/: reusable geometry; safe to delete when not building.
  • website/: independent static English pages, including the committed web fonts in website/fonts/. Successful build and expand commands refresh only the web font files; the pages stay static.

Successful builds copy their WOFF2 files, CSS, manifest and licensing into website/fonts/ by default. Use --no-web-output for experiments, or --web-output PATH for another destination. This updates local files for your next commit/deployment; it does not publish the site or change its pages.

A cold core build can take over an hour; expanding the complete family takes several hours. Progress is printed and completed work is cached. Working builds, desktop fonts and caches are excluded from Git. The verified web release is committed in website/fonts/, so website/ can be published directly without running a build. The website’s original TT2020 comparison subsets are included, with their license and provenance.

A .ttc packages separate faces; it is not a variable font. Our independently distressed contours are not compatible interpolation masters. Keep individual TTFs available for applications that do not accept collections. For the web, separate WOFF2 files let the browser fetch only the faces used.

Make a smaller experiment

uv run tt2026 build --weights 100,400,1000 \
  --styles normal --wear subtle,battered --variants 3 \
  --text " Anå.%€" --output build/experiment --no-web-output
uv run tt2026 check --build build/experiment

Use a separate output folder when comparing recipes. Omitted TOML settings inherit the approved defaults. See the configuration reference for every control and its limits.

Section What it controls
build Weights, styles, wear, character set, 1–10 impressions, seed, workers
shape Weight anchors, thinning, italic height, pooling and filling
texture Wear intensity, grain size, internal damage, smoothing, small-detail wear
pressure Finger-dependent weight and variation between strikes of the same key
outline Simplification, quadratic curve fitting and healing

The default remains deliberately opinionated. Weight 100 is fragile and faint; weight 1000 is heavy with ink. Both extremes can become difficult to read. The finger model is an artistic approximation, not a measurement of typing physiology. Ring-gap probabilities operate before later filling and healing, so they do not promise an exact number of visible gaps in every face.

Use on a website

Copy build/fonts.css and its sibling WOFF2 files, along with the license and notice, into your site:

<link rel="stylesheet" href="fonts/fonts.css">
<style>
  .typewritten {
    font-family: 'TT2026 Worn', monospace;
    font-weight: 400;
    font-synthesis: none;
    font-variant-ligatures: contextual;
    line-height: 1.4;
  }
</style>

TT2026 Subtle, TT2026 Worn and TT2026 Battered refer to wear, not weight. Select weight with font-weight and italic with font-style. All impressions advance 547 units in a 1000-unit em. The italic keeps its original outline width at 90% height, with shared line metrics. The two curly double quotes are an exception: their displaced source outlines are fitted into the character cell and aligned with the corresponding single quotes.

The font stores real alternatives. Contextual alternation (calt) chooses them deterministically according to preceding characters; spaces carry the state. It is not runtime random generation, and shaping boundaries can restart it. Repeated patterns can still repeat. Disable calt to use the first impression; combine that with cv01–cv09 to inspect another stored impression, if present:

.fixed-impression { font-feature-settings: 'calt' 0, 'cv03' 1; }

Coverage and download size

The reviewed default contains printable ASCII plus å ä ö Å Ä Ö æ Æ ø Ø ü Ü € ‘ ’ “ ” – —: 114 code points including space. A build can request additional precomposed characters present in the selected base fonts; this does not make them visually reviewed. Missing characters fail explicitly. Combining marks and control characters are rejected until a suitable positioning pipeline exists.

Build the available Latin/common package and optional script expansions from the same recipe:

uv run tt2026 expand --workers 4
uv run tt2026 check --build build/full
uv run tt2026 collection --build build/full

Expansion verifies and reuses the core fonts, builds Latin first, then adds the other available scripts. Use --scope latin for Latin only; rerun unchanged commands to resume. Each full TTF contains all built categories for its style. Web packages have non-overlapping unicode-range rules: copy build/full/fonts.css and its sibling WOFF2 files instead of the core export in the example above.

Package Upright characters Italic characters
Latin, common punctuation, currencies and symbols 352 374
Greek 92 —
Cyrillic 120 88
Armenian 89 —
Hebrew, including presentation forms 76 —
Supported source total 729 462

These are the available source characters, not complete Unicode blocks. Missing styles are not synthesized. The build excludes 38 upright and 16 italic source entries: combining marks, controls, unassigned and private-use code points. Hebrew vowel marks are not supported; use precomposed accents for Latin. The italic source lacks NBSP. The generated coverage.json records exact exclusions and per-style differences. See package details and the website’s The files table for measured sizes in the selected build.

The complete default expansion produces 210 WOFF2 packages totaling 32.09 MiB across all 60 faces. A Subtle weight-400 Latin face costs 221.4 KiB upright or 241.5 KiB italic; other categories load only when used. The desktop TTC is 70.82 MiB. Measured ranges and validation details distinguish a single face from the whole distribution.

Web fonts share base letters and accent components where source placement permits, including overlapping parts. In the v1.0.0 build, this reduced the 60 Latin files from 29.32 to 15.60 MiB (46.8%). Desktop TTF/TTC files retain the individually distressed accented glyphs; LRT retains its individual letterforms in both formats. See web accent handling.

Varying heading and paragraph starts

I vary the starting impression across headings and paragraphs, so a heading and the paragraph after it need not open with the same mark. This compact CSS-only rule uses their positions in the article. No JavaScript is needed.

.article > :is(h1, h2, h3, h4, h5, h6, p) {
  font-feature-settings:
    "calt" 1,
    "cv99" mod(sibling-index() - 1, 10);
}

sibling-index() counts from 1. Subtracting 1 makes the first child start at impression 0, and mod(..., 10) wraps the sequence through 0–9. A heading followed by two paragraphs starts at 0, 1 and 2; the eleventh child starts at 0 again. Zero selects the first stored impression. "calt" 1 keeps impressions alternating within each heading or paragraph rather than applying that starting impression to every letter.

Every sibling element counts, including images and lists, even though this rule only applies the font features to headings and paragraphs. The > selector targets direct children of .article. Nested sections and blockquotes are not part of that direct-child sequence; give a nested container the article class to start its own sequence. This is a position within each parent, not a counter carried across the whole document or from the ending impression of the previous paragraph.

The Last Remaining Typewriter has thirty starting impressions. Change 10 to 30 to cycle through 0–29. With 10, LRT still alternates through its full set inside each text block, but only the first ten starting positions are selected. CSS does the wrapping; the font does not calculate a modulus for larger values.

Inline text inherits the computed font features unless another rule overrides them. If a correction or other rule sets font-feature-settings, it replaces the whole list: retain calt and the intended cv99 value alongside the correction features. Font or style changes can also restart shaping.

Fallback for older browsers

For older browsers without sibling-index() or mod(), use these explicit rules instead of the compact rule above. They produce the same 0–9 sequence for headings and paragraphs. Here, :nth-child() deliberately counts every sibling element too; :nth-of-type() would count headings and paragraphs separately.

.article > :is(h1, h2, h3, h4, h5, h6, p) {
  font-feature-settings: "calt" 1, "cv99" var(--start, 0);
}

.article > :nth-child(10n + 1) { --start: 0; }
.article > :nth-child(10n + 2) { --start: 1; }
.article > :nth-child(10n + 3) { --start: 2; }
.article > :nth-child(10n + 4) { --start: 3; }
.article > :nth-child(10n + 5) { --start: 4; }
.article > :nth-child(10n + 6) { --start: 5; }
.article > :nth-child(10n + 7) { --start: 6; }
.article > :nth-child(10n + 8) { --start: 7; }
.article > :nth-child(10n + 9) { --start: 8; }
.article > :nth-child(10n) { --start: 9; }

The --start custom property supplies the selected value to cv99; var(--start, 0) defaults to 0 if it is unset. For all thirty LRT starts, expand this fallback to thirty 30n rules with values 0–29.

See starting-impression details.

The specimen reports measured WOFF2 bytes for the selected face. Defining 60 faces in CSS does not require downloading all 60. The specimen displays all ten weights in upright and italic by default, loading twenty Latin faces at the selected wear level. Ordinary text pages still load only the faces they use.

Why these changes?

Brennan’s TT2020 explanation asked why a typewriter font should repeat exactly the same distress. His solution stored nine glyph versions and used contextual substitutions, crediting Skef Iterum’s help with the class-based scheme. His documented workflow combined FontForge, seeded GIMP/GEGL bitmap processing and tracing. TT2026 retains the base letterforms and the OpenType idea, with a new Python build pipeline.

Anders Milton’s specimen comparisons shaped the new defaults:

  • Broader, connected wear replaced excessively fine noise. Internal losses are rarer than edge damage and can open through the edge.
  • Heavier ink pools and fills small holes; remaining large losses are irregular rather than uniform grooves.
  • Small details stay recognizable, without becoming immune to wear. Punctuation gets a restrained treatment; pinholes and hairline scratches are healed.
  • Pressure varies by key and strike, with stronger finger differences at greater wear. Accents generally follow their base letter; æ → a, ø → o, € → e. Curly quotes follow the quote key, and en/em dashes follow the hyphen key.
  • The italic uses 90% height and original width, chosen through comparisons in running text at line-height 1.4.
  • Aggressive simplification plus quadratic curves keeps files compact without leaving every outline visibly polygonal.

The English website develops this story and demonstrates the results. Its prose uses TT2026 Subtle at weight 400, 18 px with line-height 1.4, with heavier TT2026 headings; code and small controls also use TT2026. Preview the committed site with python3 website/preview.py, or pass --fonts build/full to inspect a new build. The original TT2020 is available in Anders’s mirror and Brennan’s repository.

The Last Remaining Typewriter

A separate design imagines a machine that is still being used despite its accumulated faults. Chips and debris stay with a printing face; bends and alignment biases belong to a typebar shared by its uppercase/lowercase or shifted-symbol pair. Each strike varies around those lasting faults. Uneven pressure darkens one side and weakens the other, sometimes leaving a fragment of the type’s edge. Selected basket neighbours can knock or nudge one another sideways, producing partial impressions. This is an artistic model, not a mechanical reconstruction.

Lighter and middle weights can include a faint first attempt beneath a correction; heavier weights gain occasional rebounds and broader pressure variation. Upright and italic represent separate machines with different fault patterns. Gentler italic damage protects its narrow joins, with optical thinning to balance its darkness. The reading sample defaults to one style at a time.

Twenty faces cover ten weights and both styles, with thirty stored impressions of a 114-character core. OpenType alternates the outlines while advances and line metrics stay fixed. Built-in correction features provide thirty x, dash and underscore impressions with mark sequences shuffled independently from the letters. LRT stays on its own specimen page; the build notes describe the settings and current coverage.

In memory of Fredrick Brennan, 1994–2026

Rest in peace, Fredrick. From helping create 8chan to opposing the harmful culture and far-right movements around it, your life showed a difficult willingness to reconsider. That does not erase earlier harm. Your programming and type design, while living with osteogenesis imperfecta, continue to inspire this project. Thank you—and good luck, wherever the next journey leads.

See his 2020 interview, his opposition to QAnon and 8chan, his account of living with osteogenesis imperfecta and Arthur Jones’s remembrance. Source notes are in docs/research.md.

Development

Historical font comparisons live in experiments/, outside the published website/ folder.

uv run pytest
uv run ruff check src tests
uv run ruff format --check src tests
uv build

Tests cover fresh geometry generation, deterministic rebuilding, actual HarfBuzz shaping, fixed advances, variants, extremes of weight/wear, font metadata, source checksums and TTC round trips. Fixed timestamps and seeds support reproducibility in the same locked environment; floating-point libraries or platforms can still affect the result.

Maintainers can package verified builds with uv run --group dev python tools/release.py; see release preparation.

The project structure is intentionally small:

src/tt2026/    CLI, configuration, pipeline, OpenType and source outlines
presets/      small starting recipes
website/      static pages, CSS and preview controls
docs/         configuration and historical source notes
tests/        build and shaping contracts

License and attribution

SIL Open Font License 1.1. TT2020 © 2020 Fredrick R. Brennan; TT2026 modifications © 2026 Anders Milton. Bundled sources and original comparison fonts retain their license and checksums. TT2026 uses new font family names. Keep the license and attribution with redistributed fonts.

Typed corrections

Every weight and style includes x overprints (ss01), dashed strike-through (ss02), and typed underscores (ss03). Use one of these features at a time, together with calt. Marks use the text colour by default; add ss04 for translucent red ink. Regular TT2026 has ten impressions per correction, LRT thirty. Corrections cover the Latin/common-symbol package and LRT’s core.

.crossed { font-feature-settings: "calt" 1, "ss01" 1; text-decoration: none; }
.struck { font-feature-settings: "calt" 1, "ss02" 1; }
.underlined { font-feature-settings: "calt" 1, "ss03" 1; text-decoration: none; }
.red-correction { font-feature-settings: "calt" 1, "ss01" 1, "ss04" 1; }

The ordinary TTFs and WOFF2s contain the corrections: no extra family or file is needed. Reusable zero-advance glyphs preserve character widths and the underlying text. The coloured option uses normal transparency, not Multiply blending. Desktop applications need stylistic-set controls; colour additionally needs COLR/CPAL support. Mechanics, build and verification.

Choosing correction colours

Enable ss04 to use coloured correction ink, then override palette entry 0 with @font-palette-values. This example makes the x marks blue at 80% opacity while the letters keep their ordinary CSS color. No font rebuild or extra download is needed.

@font-palette-values --blue-corrections {
  font-family: "TT2026 Worn";
  override-colors: 0 rgb(40 100 210 / 80%);
}

.blue-correction {
  font-family: "TT2026 Worn", monospace;
  font-feature-settings:
    "calt" 1,
    "cv99" var(--start, 0),
    "ss01" 1,
    "ss04" 1;
  font-palette: --blue-corrections;
  text-decoration: none;
}
<del class="blue-correction">This needs correcting.</del>

Change rgb(40 100 210 / 80%) to the colour and opacity you want. The palette override replaces the default translucent red; its opacity is not multiplied by the default red’s opacity. Use 100% for opaque ink. The palette name is used directly in font-palette, without var().

The font-family in the palette rule must match the family used by the text. Replace TT2026 Worn in both places with TT2026 Subtle, TT2026 Battered or TT2026 The Last Remaining Typewriter as needed. The override applies across that family’s weights and both styles. To use the same palette name with several families, repeat the @font-palette-values block for each family.

Replace "ss01" 1 with "ss02" 1 for dashes or "ss03" 1 for underscores, keeping "ss04" 1 enabled. Without ss04, the marks follow the text colour and ignore the palette. The cv99 setting preserves the paragraph’s selected starting impression; it defaults to 0 when --start is unset.

Custom palettes require browser support for colour fonts and @font-palette-values. If palette overrides are unavailable but colour fonts work, the marks keep their built-in red. These CSS palette rules apply on the web; desktop colour controls depend on the application.