落梅听风雪,隔窗枕雨眠
← 归去来兮隔窗听雨

数字印章:用代码刻一方篆印

2026/4/19 · tech · 阅读 - 次

方寸之间,气象万千。一枚印章,是书画作品的最后一笔,也是第一声回响。

印章之道

中国印章有阴阳之分。阴章(白文)字凹刻,底色为朱砂红,文字留白;阳章(朱文)字凸刻,文字显红,底色留白。这两种形制延续了数千年,从秦汉封泥到明清闲章,从未断绝。

shuimo-core 中,这一对概念被映射为 type: "yin"type: "yang",一行配置便可在阴阳之间切换。

从文字到路径

印章生成的第一步是文字排版。传统篆刻遵循竖排右起的布局,每一列自上而下,各列从右向左排列。算法以 fontSize 为基准单位,所有间距与尺寸均按比例缩放:

typescript
// 列宽   = fontSize × 0.55(估算)或浏览器 getBBox / fontkit 实测
// 字高   = fontSize × 1.1(含上下安全间隔)
// 列间距 = fontSize × 0.02(columnSpacing 默认值)
// 字间距 = fontSize × 0.05(characterSpacing 默认值)
// 内边距 = fontSize × 0.02(paddingX / paddingY 默认值)

这种以字号驱动的架构确保了印章在任意尺寸下都能保持比例协调——放大到匾额或缩小到落款,气韵不变。每一个参数都还提供 *Px 后缀的绝对值版本(如 paddingXPxcornerRadiusPx),需要钉死像素时随时可以覆盖。

边框与噪声

真实的印章,边缘从不完美。岁月侵蚀、刀法顿挫、石质疏密,都会在边框上留下不规则的痕迹。shuimo-corePerlin 噪声来模拟这种天然的残破感。

核心参数有三个:

  • noiseAmount:噪声幅度,控制边框的「破损程度」
  • borderPoints:采样点数,决定边缘的「粗糙精度」
  • cornerRadius:圆角半径,影响边框转折处的形态
typescript
// 噪声只取法线的「向内分量」——只有凹陷,没有凸起
// 角落处噪声趋零,保证四角完整——这与真实印章的磨损规律一致
const cornerFactor = edgeProgress // 0→边角, 1→边中
const offset = noise * cornerFactor // 角落无噪声,边中最大

// 四角圆角各自独立、±20% 随机偏移,避免几何上的完美对称
const cornerRadii = corner * (1 + (rand() - 0.5) * 0.4)

这一设计暗合古人的审美直觉:印章残破,角必完存。

五种形制

印章并非只有方形。shuimo-core 支持五种形状:

形状说明
auto不规则梯形,左右高度微异,最接近手工篆刻
square正方形加噪声
rectangle长方形加噪声
circle圆形,沿径向施加噪声
ellipse胶囊形,直边与弧端组合

其中 auto 模式最有意思——它让左右两边的高度略有不同,产生一种微妙的不对称感,恰似刀刻石面时难以完全对齐的那种手感。

刻字三法

文字的雕刻风格通过 textCarving 控制,分为三种:

  • normal:清晰可读,如新刻之印
  • strong:边缘粗粝,如久经使用
  • stone-cut:碎裂质感,如汉印出土

stone-cut 模式最为精妙。它通过 fontkit 将字体(包括 woff2 的 brotli 编码)解析为字形路径,再用 angularizeGlyphPath() 把曲线量化到 1.4px 网格、对控制点施加约 0.9px 的伪随机抖动,模拟刀刻石面时崩裂的碎片。这不是滤镜,而是对路径本身的改造——刻刀真的下到了字形里。

三种刀法对应三套不同的滤镜参数:coreErode(保留字芯的腐蚀半径)、edgeFrequency / edgeDisplacement(边缘湍流的密度与位移)、chipFrequency / chipThreshold(崩片的频度与阈值)、以及 grainFrequency(颗粒底纹)。stone-cut 还额外接入了一段 feComponentTransfer 的 discrete 表,把湍流值阶梯化,让崩裂边像真的被凿出来一样有棱角。

字形 Worker:让主线程别等

woff2 字体里的 brotli 解码并不便宜——一份字库首次解析常常会阻塞主线程数百毫秒。shuimo-core 把整段 fontkit 解析搬进了 Web Worker:

typescript
const stamp = new Stamp({
  text: ['落梅,听风,雪'],
  fontUrl: '/fonts/zhanku-zhuanshu.woff2',
  fontWorker: new Worker(
    new URL('@jobinjia/shuimo-core/stamp/font-worker', import.meta.url),
    { type: 'module' },
  ),
})

Worker 端按去重后的字符表回传 commandsAtUnitsPerEm——已在 unitsPerEm 设计网格下归一好的字形指令。主线程只需 fontSize / unitsPerEm 一次缩放即可还原任意尺寸,完全不必让 fontkit 出现在 UI 线程。同一 fontUrl + 字符集 还会被 FONT_CACHE 记住,重盖一次时连消息往返都省了。

SVG 滤镜:墨与石的质感

最终的质感由三层 SVG 滤镜叠加而成:

  1. stamp-ink-texture:模拟印泥质感,用分形湍流产生墨色浓淡不均的效果
  2. stamp-border-texture:边框的石质纹理
  3. stamp-text-texture:文字的刻蚀效果,包含侵蚀、崩片和颗粒遮罩
xml
<!-- 分形湍流 + 位移映射 = 自然的墨色晕散 -->
<feTurbulence type="fractalNoise" baseFrequency="0.04" />
<feDisplacementMap scale="10" />
<!-- 印泥位移后用 feComposite operator="in" 裁回原始轮廓,
     保证「只凹不凸」——这一刀切下去,边只会更内敛 -->

所有 baseFrequencyscale 都按 fontSize / 70 等比缩放(代码里叫 filterScale),让 35px 的小印和 280px 的大印都能呈现一致的纹理密度。三层滤镜各司其职,叠加后便是一枚有岁月感的印章。

种子与确定性

印章生成引入了 seed 参数。同一个种子,无论在浏览器还是 Node.js 中,都会生成完全一致的结果。所有浮点数通过 fmtNum() 格式化为固定精度,噪声由种子驱动的伪随机数生成器控制。

这意味着一枚数字印章可以像真实印章一样——每次盖下去,都是同一方印。

WASM:把 Perlin 搬进 Rust

最初版本的边框噪声完全跑在 JS 里——三线性插值、12 方向梯度、几百次 noise3D 调用,一次盖章就把主线程顶到 16ms 边缘。后来 shuimo-core 写了一个 Rust crate shuimo-noise,把 square / rectangle / circle / ellipse 四种规则形状的整段边框生成都搬到 WebAssembly 里:

typescript
// StampWasm.ts —— 没有 fetch,没有副本
const bin = atob(WASM_NOISE_BASE64)           // 产物内嵌为 base64
const buf = new Uint8Array(bin.length)
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i)
initSync({ module: buf.buffer })              // 现代 initSync 签名

const path = stamp_circle_path(
  radius, borderPoints, noiseAmount,
  seed, regular, /*octaves=*/4, /*persistence=*/0.5,
)

WASM 模块作为 base64 直接打进 JS 产物——没有额外的网络请求,没有 MIME 类型烦恼,没有 CORS 折磨。auto 梯形与字形渲染还留在 JS 端(毕竟那里需要和 SVG 字符串拼接),但纯计算密集的循环已经交给原生代码。

尾声

印者,信也。

古人以印为信,凭章为凭。今天我们用 Perlin 噪声模拟石质的残破,用贝塞尔曲线描摹篆书的筋骨,用 SVG 滤镜重现印泥的温润。技术在变,但那方寸之间的庄重与信诺,从未改变。

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.4.1
归去来兮 ←