缘起:typeof.net 的三篇数学文章

一切始于 typeof.net,Iosevka 作者李仁智(be5invis)的个人博客。三篇文章满屏公式,足以劝退点进来的普通读者,但拆开看,它们其实都在讲同一件事:可变字体(Variable Fonts,OpenType Font Variations)

  • 传统字体:细体、常规体、粗体要画三套完全不同的字形文件(ttf/otf);
  • 可变字体:只画一套基础字形,再指定几个"轴"(字重轴、字宽轴、倾斜轴),拖动滑块字体就像橡皮泥一样平滑变形。

官方标准(微软、Adobe 等)用的是线性插值——字体的变化"直来直去",中间状态会扭曲难看。作者用三篇文章一步步挑战这个现状:

  1. Analyzing Variation Models(2019):先把问题说清楚——把官方线性模型的局限用代数公式精确建模。好比让一个圆变成方,官方做法是直接把边缘直线拉伸,中间状态必然歪七扭八。作者把这种"不自然"写成公式,为后面的改进铺路;

  2. On High-order Interpolation and Non-linear Interpolation(2021):看清问题后开始动手改造:官方标准只允许乘法运算(轴与轴的乘积)、不允许平方,作者就"骗"一下电脑——建立两个数值同步的轴(v₀ = v₁ = t),v₀ × v₁ 在数学上悄悄实现了 t²。不用改任何渲染引擎,字体就能沿着二次曲线平滑过渡,从"直来直去"升级成"会拐弯";

  3. Integrals of variable values(2022):字体终于能"拐弯"之后,作者继续追问:任意瞬间它的面积(“墨水消耗量”)和边界怎么算?这直接决定了字体变粗变细时视觉重量是否均匀,也让文字沿曲线排版的动画严丝合缝。

这些研究最终落到了 Iosevka 身上:它的字形是从代码生成的——GitHub 主页 那句 “Versatile typeface for code, from code.” 说的就是这个意思。

Iosevka 是什么

  • 极致的瘦:等宽家族默认字符宽度只有 1/2 em,同屏能塞下比其他编程字体多约 20% 的代码列数;
  • 消灭混淆1Il0O 等盲区字符全部通过字形变体拉开差距(名字首字母是 I 不是 l,本身就是个活招牌);
  • 任意定制:144 个可配置字符 × 19 种样式集(Stylistic Sets,可一键"高仿" Consolas / Menlo / Source Code Pro / JetBrains Mono 等)。不想手写 TOML 的话,官网有 网页定制器,挑选字形变体后生成对应的 private-build-plans.toml 配置;
  • 活跃维护:2026 年 8 月已发布到 v34.8.0,累计 425 次 Releases,GitHub 22.6k+ stars;
  • 衍生生态:与思源黑体融合出 更纱黑体(Sarasa Gothic) 解决中英文混排对齐;Nerd Fonts 官方仓库长期维护打好图标补丁的版本。

家族有 6 个等宽子系列 + 2 个比例系列:默认版、Term(终端)、Fixed(无连字)各带一个粗衬线的 Slab 版(如 Iosevka Slab),外加 Iosevka Aile / Etoile 两个网页正文用的准比例字体。顺带一提,Iosevka 这个名字取自《血源诅咒》的 NPC 约瑟夫卡(Iosefka)——斯拉夫语里给"约瑟夫"加上昵称后缀 -ka,作者起名时显然没走严肃路线。

字体处理与生成的核心概念

定制 Iosevka 前,先分清几组容易混淆的概念——它们正是大多数人第一次打开配置文件就卡壳的地方。

spacing(间距模式,“规矩”)vs width(字宽,“身材”)

  • spacing 决定电脑如何看待字符的占位:
    • normal:标准等宽,但全角符号(中文标点、∑ 等)允许占 2 格——适合编辑器;
    • term无条件严格等宽,任何符号都压进单倍宽度——终端首选,杜绝光标漂移;
    • quasi-proportional:打破等宽,m 宽 i 窄,读起来像书——网页正文专用(Aile/Etoile 系列)。
  • width 决定字母本身的胖瘦(字形网格单元,em units):
    • 500:Iosevka 的灵魂,修长身材;
    • 600:大厂标准(JetBrains Mono / Fira Code 约 600–650),饱满但"不 Iosevka"。

两者自由组合,互不干扰:term + normal = 终端终极形态;normal + extended = 编辑器舒适形态。

中英文混排的 2:1 铁律:必须 width 500

一个汉字的标准全角宽度是 1000 单元。只有 500 宽等宽字体,两个英文字母 = 1000 = 一个汉字,才能达成 2:1 完美对齐。

若用 600 宽,两个英文字母 = 1200,与中文的 1000 无法凑整——混排时中英文之间会出现忽大忽小的空隙,光标错位、表格散架。所以要写中文注释就死守 500(更纱黑体正是基于此融合的)。

sans vs slab(衬线)

  • sans:无衬线,笔画干净利落,屏幕小字号下清爽易读;
  • slab:笔画末端带方方正正的小横杠,复古打字机质感。

终端和网页正文死守 sans(与中文黑体视觉调性统一、低分屏不粘连);追求复古杂志风才用 slab(即官方 Etoile 系列)。

weights:shape / menu / css 三分工

字重遵循国际规范:400 = Regular、700 = Bold,任何系统在无额外配置时只认这两个锚点。配置里的三个属性各司其职:

属性 作用
shape 喂给字形生成数学公式的笔画粗细参数
menu 写入字体文件后,系统字体菜单里识别的字重级别
css 写入 OpenType 元数据,供 font-weight: 400/700 精确匹配

slopes:upright / italic / oblique

形态 含义 场景
upright 直立 主力字形
italic 真斜体:重新画一套偏手写风格的字形(a 变单层、f/g/y 带卷曲尾巴) 编辑器注释,大字号下优雅
oblique 伪斜体:直立骨架整体掰斜固定角度(如 9.4°),无任何字形变化 终端小字号下不粘连,清晰

软件渲染斜体只认 css = "italic" 这个标签。想让终端用几何斜、编辑器用手写斜,可以让两个家族各装各的:终端把 shape = "oblique" 的字体 css 也写成 "italic"(“狸猫换太子”),Ghostty 收到斜体信号时就去抓几何斜;编辑器老老实实 shape = "italic",注释就渲染出手写体。

hinting:ttf vs ttf-unhinted

Hinting 是嵌入字体的像素对齐指令:小字号下让笔画严格对齐物理像素,不发虚。

  • 带 hinting 的 ttf:Windows / 1080P 低分屏必选;
  • ttf-unhinted:体积减半,靠高分屏自身像素密度渲染。macOS Retina 屏会自动忽略 hinting,所以 Mac 上两者几乎无差别。

OpenType 特性导出:cv## / ss## 与 glyph names

构建时把某字符固定(如 zero = "dotted")后,默认会丢弃其他样式以缩减体积。导出开关可以把所有 cv##(单字符变体)和 ss##(风格套装)的 OpenType 特性标签打包进字体:

  • 之后可在 VS Code 里用 "editor.fontLigatures": "'cv26': 6, 'ss14'" 动态切换字形,不用重新编译;
  • exportGlyphNames 为 Kitty 等终端保留字形名称:默认构建为了压缩体积会把字形名抹成数字 ID,导致 Kitty 无法识别符号(官方文档原话:“this is necessary for ligature support in Kitty”)。

定制项解释:我的 private-build-plans.toml

我按场景拆成四个构建计划,源码见 iosevka-custom

# ==================== 1. 终端字体(Ghostty):term 严格等宽 · Oblique 几何斜 ====================
[buildPlans.IosevkaTerminal]
family = "Iosevka Terminal"
spacing = "term"                  # 任何符号都压进单倍宽度,杜绝光标漂移
serifs = "sans"                   # 无衬线,小字号不粘连
noCvSs = false
exportGlyphNames = true           # 保留字形名:Kitty 连字依赖

# ---- 字形定制:消灭盲区字符 ----
[buildPlans.IosevkaTerminal.variants.design]
zero = "dotted"                   # 0 带点,区分大写 O
a = "single-storey-serifless"     # 单层闭环 a

# ---- 字重:只留 400/700 两个系统锚点 ----
[buildPlans.IosevkaTerminal.weights.Regular]
shape = 400
menu = 400
css = 400

[buildPlans.IosevkaTerminal.weights.Bold]
shape = 700
menu = 700
css = 700

# ---- 字宽:500,中英文 2:1 对齐的唯一正解 ----
[buildPlans.IosevkaTerminal.widths.Normal]
shape = 500
menu = 5
css = "normal"

# ---- 斜体:终端小字号下几何斜不粘连 ----
[buildPlans.IosevkaTerminal.slopes.Upright]
angle = 0
shape = "upright"
menu = "upright"
css = "normal"

[buildPlans.IosevkaTerminal.slopes.Oblique]
angle = 9.4
shape = "oblique"
menu = "oblique"
css = "oblique"

# ==================== 2. 编辑器字体(VS Code):normal 等宽 · Italic 手写斜 · 无连字 ====================
[buildPlans.IosevkaCode]
family = "Iosevka Code"
spacing = "normal"                # 全角符号允许占 2 格,语法高亮更自然
serifs = "sans"
noCvSs = false
exportGlyphNames = true
noLigation = true                 # 关闭连字:`==` 就是 `==`,排错一眼数清

# ---- 字形定制:消灭盲区字符 ----
[buildPlans.IosevkaCode.variants.design]
zero = "dotted"                   # 0 带点,区分大写 O
a = "single-storey-serifless"     # 单层闭环 a

# ---- 字重:只留 400/700 两个系统锚点 ----
[buildPlans.IosevkaCode.weights.Regular]
shape = 400
menu = 400
css = 400

[buildPlans.IosevkaCode.weights.Bold]
shape = 700
menu = 700
css = 700

# ---- 字宽:500,中英文 2:1 对齐的唯一正解 ----
[buildPlans.IosevkaCode.widths.Normal]
shape = 500
menu = 5
css = "normal"

# ---- 斜体:编辑器大字号下手写斜优雅 ----
[buildPlans.IosevkaCode.slopes.Upright]
angle = 0
shape = "upright"
menu = "upright"
css = "normal"

[buildPlans.IosevkaCode.slopes.Italic]
angle = 9.4
shape = "italic"
menu = "italic"
css = "italic"

# ==================== 3. 网页正文(博客文章):quasi-proportional 比例字体 ====================
[buildPlans.IosevkaWeb]
family = "Iosevka Web"
spacing = "quasi-proportional"    # 打破等宽,m 宽 i 窄,读起来像书
serifs = "sans"
noCvSs = true                     # 导出全关,把 woff2 体积压到几十 KB
exportGlyphNames = false

# ---- 字形定制:消灭盲区字符 ----
[buildPlans.IosevkaWeb.variants.design]
zero = "dotted"                   # 0 带点,区分大写 O
a = "single-storey-serifless"     # 单层闭环 a

# ---- 字重:网页正文只留常规体 ----
[buildPlans.IosevkaWeb.weights.Regular]
shape = 400
menu = 400
css = 400

# ---- 字宽:500 ----
[buildPlans.IosevkaWeb.widths.Normal]
shape = 500
menu = 5
css = "normal"

# ---- 斜体:只留直立 ----
[buildPlans.IosevkaWeb.slopes.Upright]
angle = 0
shape = "upright"
menu = "upright"
css = "normal"

# ==================== 4. 网页代码块(博客高亮):等宽 · 默认连字 ====================
[buildPlans.IosevkaWebCode]
family = "Iosevka Web Code"
spacing = "normal"                # 代码块必须是老实等宽
serifs = "sans"
noCvSs = true
exportGlyphNames = false          # 网页端不需要 Kitty 字形名
# 不写 ligations 块 = 默认开启全套连字(网页展示代码连字是加分项)

# ---- 字形定制:消灭盲区字符 ----
[buildPlans.IosevkaWebCode.variants.design]
zero = "dotted"                   # 0 带点,区分大写 O
a = "single-storey-serifless"     # 单层闭环 a

# ---- 字重:常规 + 粗体,供关键字高亮 ----
[buildPlans.IosevkaWebCode.weights.Regular]
shape = 400
menu = 400
css = 400

[buildPlans.IosevkaWebCode.weights.Bold]
shape = 700
menu = 700
css = 700

# ---- 字宽:500 ----
[buildPlans.IosevkaWebCode.widths.Normal]
shape = 500
menu = 5
css = "normal"

# ---- 斜体:直立 + 手写斜,供代码注释高亮 ----
[buildPlans.IosevkaWebCode.slopes.Upright]
angle = 0
shape = "upright"
menu = "upright"
css = "normal"

[buildPlans.IosevkaWebCode.slopes.Italic]
angle = 9.4
shape = "italic"
menu = "italic"
css = "italic"

四个计划的定位:

计划 场景 关键差异
IosevkaTerminal Ghostty 终端 spacing = "term" 严格等宽;斜体用 Oblique(9.4° 几何斜,小字号不粘连);关闭连字(终端里 ===== 必须数得清字符数)
IosevkaCode VS Code 编辑器 spacing = "normal";斜体用 Italic(手写体渲染注释);同样关闭连字(我偏好"诚实"的字形,- > 就是 - >,排错时一眼数清)
IosevkaWeb 博客网页正文 spacing = "quasi-proportional" 比例字体;只留 Regular + Upright,导出全关,把 woff2 体积压到几十 KB
IosevkaWebCode 博客网页代码块 spacing = "normal" 等宽;默认开启连字(网页展示代码连字是加分项);Regular/Bold/Italic 齐全供代码高亮插件使用

字重、字宽、斜体的骨架搭好后,字形细节只剩两条定制,恰好打在盲区字符的痛点上:

  • zero = "dotted":数字 0 中间带点,与字母 O 彻底区分;
  • a = "single-storey-serifless":单层闭环小写 a,圆形 + 一根直线,极简现代。

如何生成

# 1. 获取源码(可选用国内镜像,全套 Releases 体积巨大)
git clone --depth 1 https://github.com/be5invis/Iosevka.git
cd Iosevka

# 2. 放入定制配置(仓库根目录)
#    把上面这份 private-build-plans.toml 放进来

# 3. 安装依赖(首次必须,否则报 verda: command not found)
bun install

# 4. 构建
bun run build -- ttf::IosevkaTerminal
bun run build -- ttf::IosevkaCode
bun run build -- contents::IosevkaWeb
bun run build -- contents::IosevkaWebCode

三个构建目标(<target>::<plan>)的区别:

目标 输出 用途
ttf:: 带 Hinting 的 .ttf 系统安装
ttf-unhinted:: 无 Hinting 的 .ttf 高分屏
contents:: .ttf + .woff2 + .css(含写好的 @font-face) 网页字体全家桶

安装哪些字体文件

macOS 把 .ttf 双击或放入 ~/Library/Fonts/ 即可。构建产出的目录树(v34.8.0):

iosevka-custom/
├── IosevkaTerminal/TTF/          # → 安装到 ~/Library/Fonts/
│   ├── IosevkaTerminal-Regular.ttf
│   ├── IosevkaTerminal-Bold.ttf
│   ├── IosevkaTerminal-Oblique.ttf
│   └── IosevkaTerminal-BoldOblique.ttf
├── IosevkaCode/TTF/              # → 安装到 ~/Library/Fonts/
│   ├── IosevkaCode-Regular.ttf
│   ├── IosevkaCode-Bold.ttf
│   ├── IosevkaCode-Italic.ttf
│   └── IosevkaCode-BoldItalic.ttf
├── IosevkaWeb/                   # → 部署到博客 /fonts/
│   ├── IosevkaWeb.css
│   └── WOFF2/
│       └── IosevkaWeb-Regular.woff2
└── IosevkaWebCode/               # → 部署到博客 /fonts/
    ├── IosevkaWebCode.css
    └── WOFF2/
        ├── IosevkaWebCode-Regular.woff2
        ├── IosevkaWebCode-Bold.woff2
        ├── IosevkaWebCode-Italic.woff2
        └── IosevkaWebCode-BoldItalic.woff2

要点:

  • 终端 + 编辑器共 8 个 .ttf 装进系统,其中 Bold 是原生粗体(700 shape),避免系统算法"拉肥"的伪粗体发虚;
  • 网页字体用 .woff2 部署到博客 /fonts/,配 IosevkaWeb.css / IosevkaWebCode.css 里写好的 @font-face 直接引入;
  • 一个 width 输出一个独立家族(多 width 需要装多份、靠字体名切换),所以只保留 500 宽这一份——它是中英文 2:1 对齐的唯一正解。

这套字体现在就在本博客生效:正文(font-size: 1.02rem,约 16px)用 Iosevka Web,代码块与行内代码(约 15px)用 Iosevka Web Code,中文回退 Sarasa Mono SC 维持 2:1 对齐——你现在读到的每个字符、每个代码块,都是它的渲染效果。

如何配置生效

Ghostty 终端

配置文件位于 $HOME/Library/Application Support/com.mitchellh.ghostty/config

# brew install --cask font-maple-mono-nf-cn
# brew install --cask font-monaspice-nerd-font
font-family = Iosevka Terminal
font-family = MonaspiceRn Nerd Font Mono
font-family = Maple Mono NF CN
font-size = 22

Ghostty 支持多行 font-family 形成回退链:主字体是自编译的 Iosevka Terminal,按需回退到带图标的 Nerd Font 字体(Maple Mono NF CN 自带中文,是中文回退的关键一环)。Cmd+Shift+, 可热重载配置。

VS Code(settings.json)

{
  "editor.fontLigatures": true,
  "editor.fontFamily": "Iosevka Code, 'Input Mono', Menlo, Monaco, 'Courier New', monospace",
  "editor.fontSize": 15,
  "editor.lineHeight": 1.7
}
  • fontFamily逗号分隔的回退链,VS Code 从前往后取第一个系统里存在的字体;
  • 带空格的字体名按官方规则用单引号包裹(VS Code 默认值 'Droid Sans Mono', 'monospace', ... 即如此);
  • 想让注释强制渲染手写斜体,用 textMateRulescomment 作用域设为斜体(fontStyle 合法值:italic / bold / underline / strikethrough,可空格组合):
{
  "editor.tokenColorCustomizations": {
    "textMateRules": [
      {
        "scope": ["comment", "punctuation.definition.comment"],
        "settings": { "fontStyle": "italic" }
      }
    ]
  }
}

验证:中英文混排 + 粗斜体测试

# test.sh —— 4 行同时触发 正常/粗/斜/粗斜,检查 2:1 对齐与字形特征
echo -e "正常体: a g i l o 0 O | const val = 0; 中英文 2:1 黄金对齐" && \
echo -e "\033[1m纯粗体: a g i l o 0 O | const val = 0; 汉字加粗清晰测试\033[0m" && \
echo -e "\033[3m纯斜体: a g i l o 0 O | const val = 0; 手写弧度跨族调用\033[0m" && \
echo -e "\033[1;3m粗斜体: a g i l o 0 O | const val = 0; 终极复合无缝渲染\033[0m"

竖着看:a/=/0/「中」都在同一条垂直线上,4 行对齐严丝合缝——2:1 纪律成立。

踩坑记录

  1. verda: command not found:忘了 bun install
  2. 编译卡死:不提供 private-build-plans.toml 时默认构建官方全套计划(几百个字体文件),CPU 满载。只留 Regular/Bold + 需要的 slopes,计算量可砍 70%+。
  3. 不要装 Extended(600 宽):中英文对齐崩溃、分屏列数缩水 20%。只有纯英文场景才值得。
  4. 不要装 unhinted:Mac 上无差别,但低分屏时可能有坑,标准 ttf 永远是最优解。

从三篇劝退的数学文章,到一份 TOML 配置、8 个 TTF 文件、终端和编辑器的两套字体配置——折腾完这套流程,最大的感受是:字体不再是设计师的图纸,而是可以放进 git 的配置。