Kimi K3 如何打造数字钢笔笔画
我们想在 Lulucat Notes 里加入 Freeform 那种带方向的钢笔笔画。规格说明只是一张手写截图。经过两个模型、一个判别性问题和一个椭圆笔尖,水笔终于写出了应有的效果。研究、代码和验证由运行在 Fireworks 上的 Kimi K3 完成。

由本文介绍的 Lulucat Notes Metal 流水线渲染。
Lulucat Notes 有一支普通笔和一支荧光笔。路线图上的下一个工具是水笔——你在 Apple Notes 和 Freeform 中见过的那种带方向的钢笔:竖笔画较粗,横笔画较细。这个工具的规格说明不是一份文档,而是一张截图:三行手写文字,分别是“pencilkit”、“无边记”和“这种有方向的水笔能力”——也就是这种带方向的水笔能力。
和之前的 Metal 流水线一样,这项工作由运行在 Fireworks 上的 Moonshot 开源模型 Kimi K3 完成:测量、建模、代码和验证。人只提供截图、回答一个问题,并在真实 iPad 上判断手感。
规格说明是一张截图
静态图片无法告诉你笔画为什么细;它只能告诉你有多细、在哪里细。因此第一步是测量。我们逐行逐列扫描截图,跟踪每道笔画的中线漂移,从而得到方向,再用该角度的正弦把扫描宽度换算成真实宽度:
| 笔画 | 方向 | 真实宽度 |
|---|---|---|
| “l”的上伸笔画(“pencilkit”) | ≈ 78° | 27.3 px |
| “k”的竖干(“pencilkit”) | ≈ 90° | 28 px |
| 力的左撇 | ≈ 66° | 25.7 px |
| 草书连接笔画 | ≈ 8° | 11 px |
| 中文横画(横) | ≈ 0° | 6–11 px |
粗细比约为 2.5。用
第一版:由方向决定宽度
第一版模型很直观:为每个输入点计算方向,通过拟合曲线将方向映射为宽度,并在采集时把结果烘入该点的半径。方向采用因果估计——对弧线上最后几个点做指数衰减加权平均,并在倍角空间中计算,使移动方向反转时不会相互抵消。它只使用过去的点,因此实时笔画和提交后的笔画逐字节一致。
构建器通过了单元检查:合成的水平、竖直、45° 和反转路径都写入了理论宽度——0.99、2.52、2.00 和 0.99 点。渲染完全不需要新增代码:半径已经写入的笔画就是一串圆形印章,我们的 point-sprite 流水线本来就能绘制它们。
在 iPad 上,人只用约十秒就否决了它:“这是美术笔,不是水笔。”
两个解释都符合这张图
为什么手感不对?有两个候选解释,而截图无法区分它们:
- 方向锁定。 笔尖几何形状强制横笔变细、竖笔变粗,无论手如何移动。这是第一版实现的机制。
- 压力与速度。 钢笔由压力驱动,样本中的模式只是手写动力学:下行笔自然会按得更重,连接笔画自然更快、更轻。
两种解释都能产生横细竖粗的截图。区别在于用力按横笔时会发生什么。第一版会让它保持细;压感笔会把它变粗。因此我们只问人一个问题:用力按下的横笔应该变粗吗?
“不。横笔保持细。”
方向锁定得到确认。但问题还没有解决,因为第一版同样锁定了方向。
答案在笔画的末端
下一条线索藏在上伸笔画的顶端。把样本里的“l”和“k”竖干放大后,可以看到两件事:直笔从头到尾保持同一宽度,笔画末端是平的斜切口——凿形笔尖抬离纸面时留下的形状。不是圆点,也不是压感收尖。

这就是人所说的“美术笔手感”。第一版建模的是样本的外观——把宽度作为估计方向的函数——而不是笔本身。方向估计器是一种传感器:输入有噪声时会抖动,遇到拐角会滞后,还会把每个笔画末端都收成圆形。真正的笔尖没有这些问题,因为它什么都不计算。宽度就是几何形状。
第二版:椭圆笔尖
最终模型完全没有方向估计器。笔尖是一个定向椭圆:长轴水平,短轴固定。沿笔画路径密集放置这个椭圆的印章,其他一切都由几何自然产生:
-
横笔沿着椭圆的开放边缘移动,所以无论压力多大,宽度始终是
——一条恒定的细线。正好符合确认过的规格。 -
竖笔穿过完整长轴:宽度为
,也就是最粗处。 -
斜笔的宽度是垂直于行进方向的椭圆弦宽,
-
笔画末端是椭圆切口——样本中平坦的笔尖形末端,无需额外处理就能得到。
-
压力只缩放长轴,
,因此压力只会增加下行笔的墨量,永远不会让横笔变粗。
根据样本测量,我们保留
渲染只需新增一个片元着色器,其他都不用改。顶点格式——位置、直径、颜色——已经包含全部信息:直径表示长轴,短轴是每个 pass 的 uniform。着色器使用与圆形印章相同的半像素覆盖率渐变计算椭圆 SDF,这让它在像素层面可与 Core Graphics 的参考实现(每个印章调用一次 fillEllipse)比较。验证走的是常规流程:包含两道钢笔笔画的合成测试集分别用两种方式渲染,再逐像素比较——结构差异为零;抽查的一道横笔在两个渲染器中都测得 12 px。

左边是第一版,右边是第二版。同样的手写输入,同一条流水线。末端说明了一切。
在 iPad 上,新笔立即通过了:“好,很好”——好。很好。
留下这个失误
第一版没有被扔掉。它是一支确实有趣的笔,只是不是水笔。因此,它以人给出的名字——漏水的圆珠笔——作为新实验笔刷菜单的第一项发布。工具栏增加了一个烧瓶按钮,点击后会弹出实验项目的文本列表;添加下一项只需在注册表中增加一行。被否决的模型只要能作为明确标注的实验发布,就不算浪费。
一个晚上的循环
完整的闭环——测量、建模、构建、上设备、提问、重新建模、重新构建、验证——花了一个晚上。运行在 Fireworks 上的 Kimi K3 负责整个技术环节:设计测量扫描,提出判别性问题而不是再次猜测,在证据转向时删除自己的方向估计器,并在改动 app 前扩展像素差异测试工具。Fireworks 的推理速度让闭环保持交互性——长篇 Metal 和 Swift diff、像素分析脚本、语料工具都来得足够快,剩下的瓶颈仍然是人的判断。
协作模式与我们在 Metal 流水线那篇文章中写的一样,而且再次奏效:模型快速、精确;人负责品味和端到端验证。一句基于手感的反馈——“美术笔,不是水笔”——就足以让模型定位准确的建模错误,并用更简单的设计替换它。
正确的模型比错误的模型更小。第二版发布时包含的活动部件比第一版少——没有估计器,没有平滑窗口,没有拟合指数。笔尖不计算宽度。笔尖就是宽度。