手写体文本生成器

把英文和数字逐字符换成 Unicode 的手写斜体、打字机等宽、黑板双线三种风格,每档都列出真实码点,并且把「代理对不能被 charCodeAt 拆开」这件事现场演示给你看。

提示:输入框里按 Ctrl + Enter(macOS 用 Cmd + Enter)立刻重新生成;单按 Enter 只换行、不触发生成。每档右上角的「复制」只取走这一档的结果本身,不带码点行。

原字符手写斜体打字机等宽黑板双线

使用方法

  1. 在文本框里输入内容,默认已经填好 Handwriting 2026。只有 A–Z、a–z、0–9 参与替换,汉字、日文、标点、emoji 原样保留。
  2. 结果区分成三行,每行是一档风格:手写斜体(数学斜体区块)、打字机等宽(等宽区块)、黑板双线(双线区块)。行内上方是档位名和「复制」按钮,中间是转换结果,下面是这一档里每个字符的真实码点。
  3. 点某一档的「复制」,取走的是该档结果本身,不含码点行和档位名,可直接粘进昵称、简介、标题或签名。
  4. 下方的码点对照表把 62 个可映射字符(大写 26、小写 26、数字 10)逐行列出,默认只显示 12 行,点「加载更多(还有 N 行)」展开全部;点「复制码点对照表」拿到制表符分隔的文本,点「导出码点 CSV」下载带 UTF-8 BOM 的表格文件。
  5. 输入框里按 Ctrl + Enter 重新生成;点「载入示例」把内容还原成 Handwriting 2026。

这个手写体文本生成器能解决什么问题

一是让昵称和简介有一点笔迹感。社交平台的昵称字段只收纯文本,不接受自定义字体,把手写的英文换成 𝐻𝑎𝑛𝑑𝑤𝑟𝑖𝑡𝑖𝑛𝑔 这样的斜体字形,是纯文本环境下唯一能做出"手写感"的办法,不用上传图片,也不会被头像框裁掉。

二是在同一句话里做局部区分。本页按字符处理,只替换字母和数字,汉字与标点保持原样,所以可以只让想突出的那段英文换成等宽或双线,其余部分不动。

三是把"码点"这件事看清楚。每档结果下面直接列出逐字符的 U+1D434 这样的码点,写正则、排查乱码、核对复制后有没有掉字时,比肉眼比对可靠得多。想知道某个字符在 UTF-16 里占几个码元,这里一眼就能查到。

四是给排版找一个不依赖字体的等宽替代。纯文本环境里没法指定字体,用等宽数学字母可以让数字串、代码片段在视觉上保持对齐。

本页不做这些事:不安装或加载任何字体文件(它是换字符,不是换字体)、不做带圈字母、不做方框或气泡围合、不做上下标、不做倒序镜像。需要带圈、上标下标、小型大写那几档,请用页面底部的「特殊字体生成器」一次看全;要把全角字母和半角字母互转,请用「全角半角转换」。

生成原理:三个区块、代理对与 7 个历史例外

这个手写体文本生成器做的事只有一句:按码点切开输入,逐个判断是不是 ASCII 字母数字,是就换成隔壁区块里 "长得像手写体" 的 Unicode 字符,再拼回去。用到的函数只有三个——切分用 Array.from(str),读码点用 str.codePointAt(0),造字符用 String.fromCodePoint(cp)。

为什么必须成对使用 codePointAt 与 fromCodePoint。本页三个区块的码点全部大于 U+FFFF,落在 Unicode 的基本多文种平面(BMP)之外。JavaScript 字符串是 UTF-16 编码的,BMP 之外的字符在字符串里被存成一对代理码元:字符 𝐴(U+1D434)的 .length 是 2,codePointAt(0) 得到 119860,而 charCodeAt(0) 得到的是 55349(0xD835)、charCodeAt(1) 得到 56372(0xDC34)——两个都不是字符本身。所以反过来造字符时也必须用 String.fromCodePoint(0x1D434) 一次造一整对;用 String.fromCharCode(0xD835) 只会造出半个代理,显示成一个无法渲染的空位。

"看起来一样宽" 并不等于码元数一样。很多人把等宽数学字母当全角字符,其实全角的 A 是 U+FF21,在 BMP 之内,只占 1 个码元;等宽的 𝙰 是 U+1D670,在 BMP 之外,占 2 个码元。把 A 和 𝙰 粘进同一个输入框,后者要吃掉前者两倍的字符预算。文本框 maxlength="200" 数的是码元:纯 ASCII 能放满 200 个,而已经转换过的结果每个字母算 2 个,实际只能放约 100 个。

三档的基址是 Unicode 定好的,但有三处必须挂修正表。手写斜体用 Mathematical Italic 区块:大写 U+1D434–U+1D44D、小写 U+1D44E–U+1D467;打字机等宽用 Monospace:大写 U+1D670–U+1D689、小写 U+1D68A–U+1D6A3、数字 U+1D7F6–U+1D7FF;黑板双线用 Double-struck:大写从 U+1D538 起、小写 U+1D552–U+1D56B、数字 U+1D7D8–U+1D7E1。

但这份连续区间里混着没分配字符的保留位。用 Unicode 属性 \p{Assigned} 严格判定,斜体小写段里 U+1D455 未分配,双线大写段里 U+1D53A、U+1D53F、U+1D545、U+1D547、U+1D548、U+1D549、U+1D551 这 7 个未分配。原因是这些字母早在更早的 Unicode 版本里单独编过码,为了不出现"一个字符两个码位",新区块给它们空着:小写 h 的真值在 U+210E,双线大写 C、H、N、P、Q、R、Z 的真值分别在 U+2102、U+210D、U+2115、U+2119、U+211A、U+211D、U+2124(都在 "Letterlike Symbols" 区块)。如果程序偷懒只写 基址 + (字母序号),遇到这 8 个字符就会吐出空码位,结果里出现一个看不见的洞。

完整示例(数值仅作演示)

示例条件就是文本框里的默认内容 Handwriting 2026:11 个字母、1 个空格、4 个数字,共 16 个字符,全部落在 ASCII 内。三档的转换结果与覆盖字符数如下:

三档各自的覆盖量是有差别的:斜体档一共只能覆盖 52 个字符(大写 26 + 小写 26,没有数字形式),等宽档和双线档各覆盖 62 个(大写 26 + 小写 26 + 数字 10)。所以 "Handwriting 2026" 在斜体档下只变了 11 个字符,在另外两档下变了 15 个——这不是漏处理,而是 Unicode 没给数学斜体配数字。

下面把开头几个字符的码点逐位写出来,方便和结果区的码点行对照:H U+0048 → 斜体 U+1D43B、等宽 U+1D677、双线 U+210D;a U+0061 → 斜体 U+1D44E、等宽 U+1D68A、双线 U+1D552;W U+0057 → 双线 U+1D54E;2 U+0032 → 等宽 U+1D7F8、双线 U+1D7DA(斜体档没有数字,仍是 U+0032)。以上码点都是固定的 Unicode 分配结果,与输入内容无关;显示好不好看则取决于你本机是否装了覆盖这三个区块的字体。

输入范围与码点对照表

下表是三个区块的覆盖范围与代表字符。覆盖数指有多少个输入字符会被真的替换;没被覆盖的(比如斜体档里的数字)一律原样保留。

档位Unicode 区块覆盖字符数大写 A小写 a数字 0
手写斜体Mathematical Italic52U+1D434U+1D44E—(无数字档)
打字机等宽Mathematical Monospace62U+1D670U+1D68AU+1D7F6
黑板双线Mathematical Double-struck62U+1D538U+1D552U+1D7D8

第二张表说明"码元数和观感无关"。同样看起来是一个宽字符,落在 BMP 内外的存储代价完全不同,这直接决定了一个文本框能装多少个字。

字符码点是否在 BMP 内UTF-16 码元数charCodeAt(0) 读到什么
AU+0041是165
AU+FF21是(全角)165313
𝙰U+1D670否255349(半个代理,无意义)
𝐴U+1D434否255349(半个代理,无意义)
𝔸U+1D538否255349(半个代理,无意义)

把 A 和 𝐴 拼成 A𝐴B 更能说明问题:这段文字只有 3 个码点,.length 却是 4。按 charCodeAt 逐位上切,切出来的是 U+0041 U+D835 U+DC34 U+0042,中间那个字符被从腰部劈成两半;按 Array.from 切,才是正确的 U+0041 U+1D434 U+0042。这就是本页全程不用 charCodeAt 的原因。

边界、兼容性与精度说明

常见问题

为什么不能用 charCodeAt 循环处理?

因为这三个区块的字符都在 BMP 之外,在 UTF-16 字符串里各占 2 个码元。charCodeAt 只能读到半个代理码元(例如 55349),拿它当字符用,结果就会从中间被劈开,出现两个无法渲染的碎片。本页统一用 Array.from 按码点切分、用 codePointAt 读、用 String.fromCodePoint 写,三者配套。

为什么斜体档里的数字没有变化?

因为数学斜体区块只给大小写字母编了码,没有数字段。Unicode 只给部分数学风格配了数字档(等宽从 U+1D7F6 起、双线从 U+1D7D8 起),斜体没有。所以 Handwriting 2026 在斜体档下只有 11 个字母被替换,4 个数字保持 ASCII,这是设计而不是漏处理。

双线档里的大写 H 为什么和别的大写不一样?

那是历史遗留。双线段的连续区间 U+1D538–U+1D551 里有 7 个码位(C、H、N、P、Q、R、Z 的位置)是保留位,真正的 ℍ 在更早编好的 U+210D。任何做这份映射的工具都绕不开,必须挂一张修正表;只写 基址 + 序号 的程序会在这 7 个字母上吐出空码位。

等宽档算不算全角字符?

不算,这点很容易搞错。全角的 A 是 U+FF21,在 BMP 内,占 1 个 UTF-16 码元;等宽的 𝙰 是 U+1D670,在 BMP 之外,占 2 个码元。它们看起来都"宽",但全角是编码设计上的宽度别名,数学等宽只是画得像等宽字体,存储代价差一倍。

为什么文本框填不满 200 个字符?

因为 maxlength 数的是 UTF-16 码元,不是字符。你粘进去的如果已经是转换过的数学字母,每个字符要占 2 个码元,200 的额度实际只够约 100 个。想放更多,请保持纯 ASCII 输入。

复制到微信、微博、抖音后样式没了,是工具的问题吗?

不是。这些平台在入库或渲染时会做一次 NFKC 归一化,把数学斜体、等宽、双线统统折叠回普通 ASCII 字母(实测本页涉及的 7 类样品全部被折叠)。折叠发生在服务端,你从自己这一端看不到,只能通过在目标平台实际发一条来验证。

结果里出现空心方框怎么修?

方框说明你当前用的字体没有这个码位的字形。换一款覆盖更全的数学字体能让你自己这端正常显示,但对方看到的仍是方框——方框取决于对方的字体链,你无法替他解决。只是在纯文本里做一点区分的话,出现少量方框通常不影响阅读。

输入的内容会传到服务器吗?

不会。这个手写体文本生成器的映射表、切分和拼回全部在页面脚本里完成,转换过程中没有任何网络请求,也不写浏览器存储。想自己确认,打开开发者工具的 Network 面板边输入边观察,看不到新请求发出。

相关工具