缘起: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 连字依赖

# 不让终端字体抢占 Emoji,交给系统彩色 Emoji 字体渲染
[buildPlans.IosevkaTerminal.subset.exclude]
textFile = "private-emoji-exclude.txt"

# ---- 字形定制:消灭盲区字符 ----
[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                 # 关闭连字:`==` 就是 `==`,排错一眼数清

# 与终端、网页版本共用同一份 Emoji 排除列表
[buildPlans.IosevkaCode.subset.exclude]
textFile = "private-emoji-exclude.txt"

# ---- 字形定制:消灭盲区字符 ----
[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

# 不让网页字体抢占 Emoji,交给系统彩色 Emoji 字体渲染
[buildPlans.IosevkaWeb.subset.exclude]
textFile = "private-emoji-exclude.txt"

# ---- 字形定制:消灭盲区字符 ----
[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 块 = 默认开启全套连字(网页展示代码连字是加分项)

# 与正文共用同一份 Emoji 排除列表
[buildPlans.IosevkaWebCode.subset.exclude]
textFile = "private-emoji-exclude.txt"

# ---- 字形定制:消灭盲区字符 ----
[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,圆形 + 一根直线,极简现代。

Emoji:从 Unicode 组合到字体回退

这次真正踩到的坑是 。终端首选字体已经换成自编译的 Iosevka,Ghostty 也配置了 Nerd Font 回退,但它仍然显示为不协调的单色符号,而不是 macOS 的彩色 Emoji。

原因不在 Ghostty,而在字体选择规则:渲染器会从字体栈开头查找,第一个声明自己有该字形的字体就胜出。Iosevka 与 Nerd Font 都覆盖了不少符号;即使一个码位在 Unicode 里具有 Emoji 属性,只要前面的字体留有轮廓字形,后面的 Apple Color Emoji 根本没有机会参与。把 Apple 的字体排在第二位有用,但不能覆盖主字体仍含 Emoji 字形的情况。

Unicode 分布

Emoji 不是一个连续的 Unicode 区段,而是散落的字符加上组合序列。主要区域如下;完整且随版本更新的清单应以 Unicode 的 Emoji 数据文件 为准:

范围 常见内容 示例
ASCII、U+00A9 可参与 Emoji 序列的普通字符 #️⃣1️⃣©️®️
U+2600–U+26FF 杂项符号 ☀️⚡️♥️♻️
U+2700–U+27BF Dingbats,传统排版装饰符号 ✂️✈️❤️
U+1F1E6–U+1F1FF Regional Indicator,国旗的 A–Z 组成码位 🇨 + 🇳🇨🇳
U+1F300–U+1F5FF 自然、物品、活动等图形符号 🌈🎉📚💻
U+1F600–U+1F64F 表情脸与手势 😀😭🙋🙏
U+1F680–U+1F6FF 交通与地图 🚀🚲🗺️🚧
U+1F900–U+1F9FFU+1FA70–U+1FAFF 较新的补充与扩展图形符号 🤖🥹🫠🪿

文字符号与 Emoji 呈现

Dingbats 原本是 Zapf Dingbats、Wingdings 一类符号字体中的装饰字形;它们后来有相当一部分成为 Emoji,但仍是普通 Unicode 符号。因此 这类字符可能有文字和 Emoji 两种呈现。不可见的 VS16(Variation Selector-16,U+FE0F)请求 Emoji 样式,VS15(U+FE0E)请求文字样式:

序列 中文含义 码位 呈现
红心符号 U+2764 Unicode 默认文字呈现;本博客会回退到 Apple Color Emoji,通常显示为彩色
❤️ 红心 Emoji U+2764 U+FE0F VS16 请求彩色 Emoji
❤︎ 红心文字 U+2764 U+FE0E VS15 请求文字样式;实际效果取决于浏览器与可用字体

ZWJ 组合 Emoji

还有些 Emoji 根本没有单一码位,而是由不可见的 ZWJ(Zero Width Joiner,U+200D)把多个图形连成一个。系统 Emoji 字体支持时,下列序列各渲染为一个图形;不支持时则退化为多个 Emoji。完整的序列清单以 Unicode 官方的 Emoji ZWJ Sequences 图表 为准,这里挑几个有意思的:

序列 中文含义 组成部分 码位
👩‍💻 女程序员 👩 女人 + ZWJ + 💻 笔记本电脑 U+1F469 ZWJ U+1F4BB
🧑‍🍳 厨师 🧑 人 + ZWJ + 🍳 烹饪 U+1F9D1 ZWJ U+1F373
🕵️‍♀️ 女侦探 🕵️ 侦探 + ZWJ + ♀️ 女性符号 U+1F575 VS16 ZWJ U+2640 VS16
🧑‍🦽 使用手动轮椅的人 🧑 人 + ZWJ + 🦽 手动轮椅 U+1F9D1 ZWJ U+1F9BD
🧑‍⚕️ 医护人员 🧑 人 + ZWJ + ⚕️ 医学符号 U+1F9D1 ZWJ U+2695 VS16
🧑‍🚀 宇航员 🧑 人 + ZWJ + 🚀 火箭 U+1F9D1 ZWJ U+1F680
🧑‍🏫 教师 🧑 人 + ZWJ + 🏫 学校 U+1F9D1 ZWJ U+1F3EB
🏳️‍🌈 彩虹旗 🏳️ 白旗 + ZWJ + 🌈 彩虹 U+1F3F3 VS16 ZWJ U+1F308
👨‍👩‍👧‍👦 四人家庭 👨 男人 + ZWJ + 👩 女人 + ZWJ + 👧 女孩 + ZWJ + 👦 男孩 U+1F468 ZWJ U+1F469 ZWJ U+1F467 ZWJ U+1F466
🧑‍🤝‍🧑 两人牵手 🧑 人 + ZWJ + 🤝 握手 + ZWJ + 🧑 U+1F9D1 ZWJ U+1F91D ZWJ U+1F9D1
👩‍❤️‍💋‍👩 两位女性接吻 👩 女人 + ZWJ + ❤️ 红心 + ZWJ + 💋 嘴唇 + ZWJ + 👩 女人 U+1F469 ZWJ U+2764 VS16 ZWJ U+1F48B ZWJ U+1F469
❤️‍🔥 火焰红心 ❤️ 红心 + ZWJ + 🔥 火焰 U+2764 VS16 ZWJ U+1F525
❤️‍🩹 修补红心 ❤️ 红心 + ZWJ + 🩹 创可贴 U+2764 VS16 ZWJ U+1FA79
🐈‍⬛ 黑猫 🐈 猫 + ZWJ + 黑色大方块 U+1F408 ZWJ U+2B1B
🐻‍❄️ 北极熊 🐻 熊 + ZWJ + ❄️ 雪花 U+1F43B ZWJ U+2744 VS16
🐦‍🔥 凤凰 🐦 鸟 + ZWJ + 🔥 火焰 U+1F426 ZWJ U+1F525
🏴‍☠️ 海盗旗 🏴 黑旗 + ZWJ + ☠️ 骷髅 U+1F3F4 ZWJ U+2620 VS16
🏳️‍⚧️ 跨性别旗 🏳️ 白旗 + ZWJ + ⚧️ 跨性别符号 U+1F3F3 VS16 ZWJ U+26A7 VS16
🐕‍🦺 服务犬 🐕 狗 + ZWJ + 🦺 救生衣 U+1F415 ZWJ U+1F9BA
🧑‍🦳 白发人 🧑 人 + ZWJ + 🦳 白发 U+1F9D1 ZWJ U+1F9B3
👨‍🦰 红发男子 👨 男人 + ZWJ + 🦰 红发 U+1F468 ZWJ U+1F9B0
🧑‍🎄 圣诞精灵 🧑 人 + ZWJ + 🎄 圣诞树 U+1F9D1 ZWJ U+1F384
🧑‍⚖️ 法官 🧑 人 + ZWJ + ⚖️ 天平 U+1F9D1 ZWJ U+2696 VS16
🧑‍✈️ 飞行员 🧑 人 + ZWJ + ✈️ 飞机 U+1F9D1 ZWJ U+2708 VS16
🧑‍🎨 艺术家 🧑 人 + ZWJ + 🎨 调色板 U+1F9D1 ZWJ U+1F3A8
🧑‍🧒 单亲带娃 🧑 人 + ZWJ + 🧒 儿童 U+1F9D1 ZWJ U+1F476
👨‍❤️‍👨 男男情侣 👨 男人 + ZWJ + ❤️ 红心 + ZWJ + 👨 男人 U+1F468 ZWJ U+2764 VS16 ZWJ U+1F468
🦸‍♀️ 女超级英雄 🦸 超级英雄 + ZWJ + ♀️ 女性符号 U+1F9B8 ZWJ U+2640 VS16
🫱🏿‍🫲🏼 肤色不同的握手 🫱🏿 深肤色右手 + ZWJ + 🫲🏼 浅肤色左手 U+1FAF1 U+1F3FF ZWJ U+1FAF2 U+1F3FC
🏃‍➡️ 向右跑的人 🏃 跑步 + ZWJ + ➡️ 右箭头 U+1F3C3 ZWJ U+27A1 VS16

Regional Indicator 与国旗

国旗又是另一种组合:🇦🇿U+1F1E6–U+1F1FF 的 Regional Indicator,不是 26 个普通的字母 Emoji。两个连续码位组成有效国家/地区代码才会合成国旗,例如 🇨🇳 = U+1F1E8 U+1F1F3🇺🇸 = U+1F1FA U+1F1F8。单独的 🇦,或无效组合 🇦🇧,显示为黑白/文字风格的区域指示符是正确行为;这不表示 Iosevka 的 Emoji 排除失效。

解决方法是把职责切开:Iosevka 只画文字、标点和代码符号;Emoji 一律缺字,再回退给系统字体。四个构建计划共用 private-emoji-exclude.txt,其中收录 Unicode Emoji 属性的精确码位范围:

# 所有 build plan 都引用同一份列表;TOML 本身不能把一个 subset 表定义为全局别名。
[buildPlans.IosevkaTerminal.subset.exclude]
textFile = "private-emoji-exclude.txt"

列表保留了 ASCII 的 #*09。它们虽然可以与组合符组成键帽 Emoji(如 1️⃣),但单独出现时仍是最常用的代码字符;完整的键帽序列会因其他 Emoji 组成部分缺字而回退。这个策略也刻意不按大块 Unicode 范围误伤箭头、数学符号和几何符号,而是只排除 Unicode 数据中标记为 Emoji 的码位。

private-emoji-exclude.txt 的完整内容

将下面内容保存为 Iosevka 仓库根目录的 private-emoji-exclude.txt,四个构建计划即可共用:

©®‼⁉™ℹ↔↕↖↗↘↙↩↪⌚⌛⌨⏏⏩⏪⏫⏬⏭⏮⏯⏰⏱⏲⏳⏸⏹⏺Ⓜ▪▫▶◀◻◼◽◾☀☁☂☃☄☎☑☔☕☘☝☠☢☣☦☪☮☯☸☹☺♀♂♈♉♊♋♌♍♎♏♐♑♒♓♟♠♣♥♦♨♻♾♿⚒⚓⚔⚕⚖⚗⚙⚛⚜⚠⚡⚧⚪⚫⚰⚱⚽⚾⛄⛅⛈⛎⛏⛑⛓⛔⛩⛪⛰⛱⛲⛳⛴⛵⛷⛸⛹⛺⛽✂✅✈✉✊✋✌✍✏✒✔✖✝✡✨✳✴❄❇❌❎❓❔❕❗❣❤➕➖➗➡➰➿⤴⤵⬅⬆⬇⬛⬜⭐⭕〰〽㊗㊙🀄🃏🅰🅱🅾🅿🆎🆑🆒🆓🆔🆕🆖🆗🆘🆙🆚🇦🇧🇨🇩🇪🇫🇬🇭🇮🇯🇰🇱🇲🇳🇴🇵🇶🇷🇸🇹🇺🇻🇼🇽🇾🇿🈁🈂🈚🈯🈲🈳🈴🈵🈶🈷🈸🈹🈺🉐🉑🌀🌁🌂🌃🌄🌅🌆🌇🌈🌉🌊🌋🌌🌍🌎🌏🌐🌑🌒🌓🌔🌕🌖🌗🌘🌙🌚🌛🌜🌝🌞🌟🌠🌡🌤🌥🌦🌧🌨🌩🌪🌫🌬🌭🌮🌯🌰🌱🌲🌳🌴🌵🌶🌷🌸🌹🌺🌻🌼🌽🌾🌿🍀🍁🍂🍃🍄🍅🍆🍇🍈🍉🍊🍋🍌🍍🍎🍏🍐🍑🍒🍓🍔🍕🍖🍗🍘🍙🍚🍛🍜🍝🍞🍟🍠🍡🍢🍣🍤🍥🍦🍧🍨🍩🍪🍫🍬🍭🍮🍯🍰🍱🍲🍳🍴🍵🍶🍷🍸🍹🍺🍻🍼🍽🍾🍿🎀🎁🎂🎃🎄🎅🎆🎇🎈🎉🎊🎋🎌🎍🎎🎏🎐🎑🎒🎓🎖🎗🎙🎚🎛🎞🎟🎠🎡🎢🎣🎤🎥🎦🎧🎨🎩🎪🎫🎬🎭🎮🎯🎰🎱🎲🎳🎴🎵🎶🎷🎸🎹🎺🎻🎼🎽🎾🎿🏀🏁🏂🏃🏄🏅🏆🏇🏈🏉🏊🏋🏌🏍🏎🏏🏐🏑🏒🏓🏔🏕🏖🏗🏘🏙🏚🏛🏜🏝🏞🏟🏠🏡🏢🏣🏤🏥🏦🏧🏨🏩🏪🏫🏬🏭🏮🏯🏰🏳🏴🏵🏷🏸🏹🏺🏻🏼🏽🏾🏿🐀🐁🐂🐃🐄🐅🐆🐇🐈🐉🐊🐋🐌🐍🐎🐏🐐🐑🐒🐓🐔🐕🐖🐗🐘🐙🐚🐛🐜🐝🐞🐟🐠🐡🐢🐣🐤🐥🐦🐧🐨🐩🐪🐫🐬🐭🐮🐯🐰🐱🐲🐳🐴🐵🐶🐷🐸🐹🐺🐻🐼🐽🐾🐿👀👁👂👃👄👅👆👇👈👉👊👋👌👍👎👏👐👑👒👓👔👕👖👗👘👙👚👛👜👝👞👟👠👡👢👣👤👥👦👧👨👩👪👫👬👭👮👯👰👱👲👳👴👵👶👷👸👹👺👻👼👽👾👿💀💁💂💃💄💅💆💇💈💉💊💋💌💍💎💏💐💑💒💓💔💕💖💗💘💙💚💛💜💝💞💟💠💡💢💣💤💥💦💧💨💩💪💫💬💭💮💯💰💱💲💳💴💵💶💷💸💹💺💻💼💽💾💿📀📁📂📃📄📅📆📇📈📉📊📋📌📍📎📏📐📑📒📓📔📕📖📗📘📙📚📛📜📝📞📟📠📡📢📣📤📥📦📧📨📩📪📫📬📭📮📯📰📱📲📳📴📵📶📷📸📹📺📻📼📽📿🔀🔁🔂🔃🔄🔅🔆🔇🔈🔉🔊🔋🔌🔍🔎🔏🔐🔑🔒🔓🔔🔕🔖🔗🔘🔙🔚🔛🔜🔝🔞🔟🔠🔡🔢🔣🔤🔥🔦🔧🔨🔩🔪🔫🔬🔭🔮🔯🔰🔱🔲🔳🔴🔵🔶🔷🔸🔹🔺🔻🔼🔽🕉🕊🕋🕌🕍🕎🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚🕛🕜🕝🕞🕟🕠🕡🕢🕣🕤🕥🕦🕧🕯🕰🕳🕴🕵🕶🕷🕸🕹🕺🖇🖊🖋🖌🖍🖐🖕🖖🖤🖥🖨🖱🖲🖼🗂🗃🗄🗑🗒🗓🗜🗝🗞🗡🗣🗨🗯🗳🗺🗻🗼🗽🗾🗿😀😁😂😃😄😅😆😇😈😉😊😋😌😍😎😏😐😑😒😓😔😕😖😗😘😙😚😛😜😝😞😟😠😡😢😣😤😥😦😧😨😩😪😫😬😭😮😯😰😱😲😳😴😵😶😷😸😹😺😻😼😽😾😿🙀🙁🙂🙃🙄🙅🙆🙇🙈🙉🙊🙋🙌🙍🙎🙏🚀🚁🚂🚃🚄🚅🚆🚇🚈🚉🚊🚋🚌🚍🚎🚏🚐🚑🚒🚓🚔🚕🚖🚗🚘🚙🚚🚛🚜🚝🚞🚟🚠🚡🚢🚣🚤🚥🚦🚧🚨🚩🚪🚫🚬🚭🚮🚯🚰🚱🚲🚳🚴🚵🚶🚷🚸🚹🚺🚻🚼🚽🚾🚿🛀🛁🛂🛃🛄🛅🛋🛌🛍🛎🛏🛐🛑🛒🛕🛖🛗🛘🛜🛝🛞🛟🛠🛡🛢🛣🛤🛥🛩🛫🛬🛰🛳🛴🛵🛶🛷🛸🛹🛺🛻🛼🟠🟡🟢🟣🟤🟥🟦🟧🟨🟩🟪🟫🟰🤌🤍🤎🤏🤐🤑🤒🤓🤔🤕🤖🤗🤘🤙🤚🤛🤜🤝🤞🤟🤠🤡🤢🤣🤤🤥🤦🤧🤨🤩🤪🤫🤬🤭🤮🤯🤰🤱🤲🤳🤴🤵🤶🤷🤸🤹🤺🤼🤽🤾🤿🥀🥁🥂🥃🥄🥅🥇🥈🥉🥊🥋🥌🥍🥎🥏🥐🥑🥒🥓🥔🥕🥖🥗🥘🥙🥚🥛🥜🥝🥞🥟🥠🥡🥢🥣🥤🥥🥦🥧🥨🥩🥪🥫🥬🥭🥮🥯🥰🥱🥲🥳🥴🥵🥶🥷🥸🥹🥺🥻🥼🥽🥾🥿🦀🦁🦂🦃🦄🦅🦆🦇🦈🦉🦊🦋🦌🦍🦎🦏🦐🦑🦒🦓🦔🦕🦖🦗🦘🦙🦚🦛🦜🦝🦞🦟🦠🦡🦢🦣🦤🦥🦦🦧🦨🦩🦪🦫🦬🦭🦮🦯🦰🦱🦲🦳🦴🦵🦶🦷🦸🦹🦺🦻🦼🦽🦾🦿🧀🧁🧂🧃🧄🧅🧆🧇🧈🧉🧊🧋🧌🧍🧎🧏🧐🧑🧒🧓🧔🧕🧖🧗🧘🧙🧚🧛🧜🧝🧞🧟🧠🧡🧢🧣🧤🧥🧦🧧🧨🧩🧪🧫🧬🧭🧮🧯🧰🧱🧲🧳🧴🧵🧶🧷🧸🧹🧺🧻🧼🧽🧾🧿🩰🩱🩲🩳🩴🩵🩶🩷🩸🩹🩺🩻🩼🪀🪁🪂🪃🪄🪅🪆🪇🪈🪉🪊🪎🪏🪐🪑🪒🪓🪔🪕🪖🪗🪘🪙🪚🪛🪜🪝🪞🪟🪠🪡🪢🪣🪤🪥🪦🪧🪨🪩🪪🪫🪬🪭🪮🪯🪰🪱🪲🪳🪴🪵🪶🪷🪸🪹🪺🪻🪼🪽🪾🪿🫀🫁🫂🫃🫄🫅🫆🫈🫍🫎🫏🫐🫑🫒🫓🫔🫕🫖🫗🫘🫙🫚🫛🫜🫟🫠🫡🫢🫣🫤🫥🫦🫧🫨🫩🫪🫯🫰🫱🫲🫳🫴🫵🫶🫷🫸

在 macOS 上,系统提供的彩色 Emoji 字体名是 Apple Color Emoji。它不是通用代码字体,正适合放在 Iosevka 之后、其他 Nerd Font 之前:Emoji 获得彩色图形,Powerline、终端图标和中文仍继续由后续字体处理。

font-family = Iosevka Terminal
font-family = Apple Color Emoji
font-family = MonaspiceRn Nerd Font Mono
font-family = Maple Mono NF CN

网页也是同一个逻辑。浏览器对一个字形或 Emoji 序列在首选 Web 字体找不到完整覆盖时,才会继续向后回退。因此正文和代码字体栈同样将 Apple Color Emoji 放在自定义 Iosevka 后面:

font-family: "Iosevka Web", "Apple Color Emoji", "InputMono", monospace;
font-family: "Iosevka Web Code", "Apple Color Emoji", "Noto Sans SC", monospace;

Apple Color Emoji 是 macOS 字体;其他系统没有它时,浏览器会继续使用各自的系统回退字体。若只做网页端,也可以用 @font-faceunicode-range 将 Emoji 范围指定给一个字体,但构建时直接排除 Emoji 更简单:终端、编辑器和网页共享同一套边界,且不会被主字体抢占。

最后是一条务必照做的构建建议:只要改了 private-emoji-exclude.txt,就先删除 Iosevka 仓库的 .build/ 缓存和相关计划的旧 dist/<plan>/ 输出,再从头构建该计划。不要依赖增量构建;textFile 的内容不是当前 Verda 构建图的显式依赖,缓存可能继续复用未排除 Emoji 的旧字体。

如何生成

# 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;Emoji 回退到 Apple Color Emoji,中文与其他缺字继续由后续字体接手——你现在读到的每个字符、每个代码块,都是它的渲染效果。

如何配置生效

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 = Apple Color Emoji
font-family = MonaspiceRn Nerd Font Mono
font-family = Maple Mono NF CN
font-size = 22

Ghostty 支持多行 font-family 形成回退链:主字体是自编译的 Iosevka Terminal;第二位的 Apple Color Emoji 专门接管 Emoji;随后才是带图标的 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 的配置。