diff --git a/.gitignore b/.gitignore index e69de29..33c9089 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,6 @@ +# Pristine Montserrat originals, re-downloadable by tools/rescale-aspect.py. +tools/_originals/ + +# Python bytecode +__pycache__/ +*.pyc diff --git a/site/fonts/montserrat-latin-400-aspect.woff2 b/site/fonts/montserrat-latin-400-aspect.woff2 new file mode 100644 index 0000000..2a073ec Binary files /dev/null and b/site/fonts/montserrat-latin-400-aspect.woff2 differ diff --git a/site/fonts/montserrat-latin-700-aspect.woff2 b/site/fonts/montserrat-latin-700-aspect.woff2 new file mode 100644 index 0000000..9c95a9c Binary files /dev/null and b/site/fonts/montserrat-latin-700-aspect.woff2 differ diff --git a/site/index.html b/site/index.html index 1fd8921..b0cf963 100644 --- a/site/index.html +++ b/site/index.html @@ -6,13 +6,10 @@ Raum und Linie + + - @@ -21,9 +18,9 @@
-

Hier entsteht eine neue Seite.

+

🚧 Hier entsteht eine neue Seite.

- Der schnellste Weg zu mir: + 📬 Der schnellste Weg zu mir: mail@raum-und-linie.de.

diff --git a/site/style/base.css b/site/style/base.css index 96ef032..33a82ff 100644 --- a/site/style/base.css +++ b/site/style/base.css @@ -1,5 +1,48 @@ -:root{ - --color-background: #faf9f7; - --font-body: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; +@font-face { + font-family: "Montserrat"; + font-style: normal; + font-weight: 400; /* 400 = Regular */ + font-display: swap; /* show the fallback first, swap in later */ + src: url("../fonts/montserrat-latin-400-aspect.woff2") format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, + U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } +@font-face { + font-family: "Montserrat"; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url("../fonts/montserrat-latin-700-aspect.woff2") format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, + U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +:root{ + --color-canvas: #faf9f7; + --color-banner-background: #435245; + --color-banner-text: orange; + --font-body: "Montserrat", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; +} + +body { + background-color: var(--color-canvas); + font-family: var(--font-body); + margin: 0px; +} + +header { + color: var(--color-banner-text); + background-color: var(--color-banner-background); + padding-left: 32px; + display: flex; + justify-content: center; +} + +main { + max-width: 42rem; + margin-inline: auto; + padding-inline: 1rem; +} diff --git a/tools/rescale-aspect.py b/tools/rescale-aspect.py new file mode 100644 index 0000000..aee86d0 --- /dev/null +++ b/tools/rescale-aspect.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Generate the aspect-adjusted Montserrat webfonts used by the site. + +The design calls for the typeface to be 110% as tall and 85% as wide as +the original Montserrat. CSS cannot do this: `font-stretch` only *selects* +a width variant from a family, it never scales, and Montserrat has no +width axis (its variable build only has `wght`). So the adjustment is baked +into the font files themselves. + +Scaling a font's aspect ratio means changing the relationship between the +glyph *outlines* and the *advance widths*. If you scale the shapes but not +the advances, the browser still reserves the original width and every line +looks loosely spaced and wraps too early. So this script scales, in order: + + 1. every glyph outline coordinate (x * 0.85, y * 1.10) + 2. every composite glyph component offset (e.g. the accent on "a-umlaut") + 3. every advance width and side bearing (x * 0.85) <- affects layout + 4. every vertical metric (ascent, descent, capHeight, xHeight) (y * 1.10) + +Non-uniform scaling invalidates TrueType hinting, so the hinting programs +and the pixel-metric tables that reference the old coordinates are dropped. + +Usage: + pip install "fonttools[woff]" # or: python3 -m venv .venv && ... + python3 tools/rescale-aspect.py + +Pristine originals are cached in tools/_originals/ (git-ignored) and +downloaded from a PINNED Fontsource version, so re-running always yields +byte-identical output. The cache is disposable: delete it any time. +""" + +import sys +import urllib.request +from pathlib import Path + +try: + from fontTools.ttLib import TTFont + from fontTools.ttLib.tables.ttProgram import Program +except ImportError: + sys.exit('fontTools is missing. Install it with: pip install "fonttools[woff]"') + +# ---- design parameters ---------------------------------------------------- +SCALE_X = 0.85 # width -> 85% +SCALE_Y = 1.10 # height -> 110% + +# Only the weights the site actually uses. Each unused weight is a file +# nobody downloads, so there is no reason to ship it. +WEIGHTS = (400, 700) + +# Pinned so the output never silently changes under you. +FONTSOURCE_VERSION = "5.3.0" +CDN = f"https://cdn.jsdelivr.net/fontsource/fonts/montserrat@{FONTSOURCE_VERSION}" + +ROOT = Path(__file__).resolve().parent.parent +CACHE = ROOT / "tools" / "_originals" +OUT = ROOT / "site" / "fonts" + +# Tables whose contents are tied to the old coordinate space. +STALE_TABLES = ("hdmx", "LTSH", "VDMX", "gasp") + + +def fetch_original(weight: int) -> Path: + """Download the pristine woff2 into the cache if it isn't there yet.""" + CACHE.mkdir(parents=True, exist_ok=True) + dest = CACHE / f"montserrat-latin-{weight}-normal.woff2" + if not dest.exists(): + url = f"{CDN}/latin-{weight}-normal.woff2" + print(f" downloading {url}") + with urllib.request.urlopen(url) as response: + dest.write_bytes(response.read()) + return dest + + +def transform(src: Path, dst: Path) -> None: + font = TTFont(src) + glyf = font["glyf"] + order = font.getGlyphOrder() + + # 1 + 2. outlines and composite offsets + for name in order: + g = glyf[name] + if g.isComposite(): + # Every level's offset is scaled; nesting composes correctly + # because (0.85*a + 0.85*b) == 0.85 * (a + b). + for comp in g.components: + comp.x = round(comp.x * SCALE_X) + comp.y = round(comp.y * SCALE_Y) + elif g.numberOfContours > 0: + # Affine-scaling every point of a quadratic outline keeps it a + # valid quadratic, so no curve conversion is needed. + coords = g.coordinates + for i in range(len(coords)): + x, y = coords[i] + coords[i] = (x * SCALE_X, y * SCALE_Y) + g.program = Program() # hinting no longer matches the coordinates + + # 3. horizontal metrics -- this is the part that makes layout reflow + hmtx = font["hmtx"] + for name in order: + advance, lsb = hmtx[name] + hmtx[name] = (round(advance * SCALE_X), round(lsb * SCALE_X)) + + # 4. vertical metrics, so the line box grows to match the taller glyphs + hhea, os2 = font["hhea"], font["OS/2"] + for table, attr in ((hhea, "ascent"), (hhea, "descent"), (hhea, "lineGap"), + (os2, "sTypoAscender"), (os2, "sTypoDescender"), + (os2, "sTypoLineGap"), (os2, "usWinAscent"), + (os2, "usWinDescent"), + (os2, "sCapHeight"), (os2, "sxHeight")): + value = getattr(table, attr, None) + if value is not None: + setattr(table, attr, round(value * SCALE_Y)) + + for table in STALE_TABLES: + if table in font: + del font[table] + + font.recalcBBoxes = True + font.save(dst) + font.close() + + +def report(path: Path) -> None: + f = TTFont(path) + cmap = f.getBestCmap() + r = f["glyf"][cmap[ord("R")]] + umlaut = f["glyf"][cmap[ord("ä")]] + print(f" {path.name} ({path.stat().st_size:,} bytes)") + print(f" 'R' advance={f['hmtx']['R'][0]:4d} capHeight={f['OS/2'].sCapHeight}" + f" xHeight={f['OS/2'].sxHeight}") + print(f" umlauts still composite: {umlaut.isComposite()}" + f" glyphs: {f['maxp'].numGlyphs}") + f.close() + + +if __name__ == "__main__": + print(f"Montserrat {FONTSOURCE_VERSION} -> height {SCALE_Y:.0%}, width {SCALE_X:.0%}\n") + OUT.mkdir(parents=True, exist_ok=True) + for weight in WEIGHTS: + original = fetch_original(weight) + target = OUT / f"montserrat-latin-{weight}-aspect.woff2" + transform(original, target) + report(target) + print("\nDone. Point your @font-face src: at the *-aspect.woff2 files.")