使用方法
- 在文本框里输入内容,默认已经填好 Handwriting 2026。只有
A–Z、a–z、0–9参与替换,汉字、日文、标点、emoji 原样保留。 - 结果区分成三行,每行是一档风格:手写斜体(数学斜体区块)、打字机等宽(等宽区块)、黑板双线(双线区块)。行内上方是档位名和「复制」按钮,中间是转换结果,下面是这一档里每个字符的真实码点。
- 点某一档的「复制」,取走的是该档结果本身,不含码点行和档位名,可直接粘进昵称、简介、标题或签名。
- 下方的码点对照表把 62 个可映射字符(大写 26、小写 26、数字 10)逐行列出,默认只显示 12 行,点「加载更多(还有 N 行)」展开全部;点「复制码点对照表」拿到制表符分隔的文本,点「导出码点 CSV」下载带 UTF-8 BOM 的表格文件。
- 输入框里按 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 内。三档的转换结果与覆盖字符数如下:
- 手写斜体档只覆盖字母,实际替换 11 个字符,4 个数字和空格原样保留,输出
𝐻𝑎𝑛𝑑𝑤𝑟𝑖𝑡𝑖𝑛𝑔 2026; - 打字机等宽档覆盖字母和数字,实际替换 15 个,输出
𝙷𝚊𝚗𝚍𝚠𝚛𝚒𝚝𝚒𝚗𝚐 𝟸𝟶𝟸𝟼; - 黑板双线档同样替换 15 个,输出
ℍ𝕒𝕟𝕕𝕨𝕣𝕚𝕥𝕚𝕟𝕘 𝟚𝟘𝟚𝟞; - 这串里唯一触发历史例外的是开头的大写
H:它在双线段的期望位置是 U+1D53F(保留位),真值是 U+210D,也就是ℍ。
三档各自的覆盖量是有差别的:斜体档一共只能覆盖 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 Italic | 52 | U+1D434 | U+1D44E | —(无数字档) |
| 打字机等宽 | Mathematical Monospace | 62 | U+1D670 | U+1D68A | U+1D7F6 |
| 黑板双线 | Mathematical Double-struck | 62 | U+1D538 | U+1D552 | U+1D7D8 |
第二张表说明"码元数和观感无关"。同样看起来是一个宽字符,落在 BMP 内外的存储代价完全不同,这直接决定了一个文本框能装多少个字。
| 字符 | 码点 | 是否在 BMP 内 | UTF-16 码元数 | charCodeAt(0) 读到什么 |
|---|---|---|---|---|
| A | U+0041 | 是 | 1 | 65 |
| A | U+FF21 | 是(全角) | 1 | 65313 |
| 𝙰 | U+1D670 | 否 | 2 | 55349(半个代理,无意义) |
| 𝐴 | U+1D434 | 否 | 2 | 55349(半个代理,无意义) |
| 𝔸 | U+1D538 | 否 | 2 | 55349(半个代理,无意义) |
把 A 和 𝐴 拼成 A𝐴B 更能说明问题:这段文字只有 3 个码点,.length 却是 4。按 charCodeAt 逐位上切,切出来的是 U+0041 U+D835 U+DC34 U+0042,中间那个字符被从腰部劈成两半;按 Array.from 切,才是正确的 U+0041 U+1D434 U+0042。这就是本页全程不用 charCodeAt 的原因。
边界、兼容性与精度说明
- 输入上限 200 个 UTF-16 码元(文本框的
maxlength)。纯 ASCII 能放满 200 个;粘贴已经转换过的结果时,每个字符占 2 个码元,实际只能放约 100 个。超过会被拦下并给出具体提示,不会静默截断。 - 只有 A–Z、a–z、0–9 参与映射。汉字没有对应的数学字母变体码位,Unicode 里不存在这类东西,所以中文输入只会得到一模一样的中文;此时结果区会明确提示本次没有可映射字符。
- 屏幕阅读器读不出来,是因为它念的是码位的官方名称。Unicode 给每个码位规定了正式名字,
ℍ的正式名是MATHEMATICAL DOUBLE-STRUCK CAPITAL H。读屏软件要么把这一长串逐字念出来,要么当无意义符号跳过,绝不会念成"字母 H"。搜索同理:用普通hand做匹配匹配的是码位序列,𝕙𝕒𝕟𝕕对不上。把这类文字用在正文、按钮、表单标签上,等于对读屏用户和搜索爬虫同时隐身。 - 平台做 NFKC 归一化时会直接打回原形。实测把这 7 个样品各过一遍
normalize('NFKC'):斜体大写 A(U+1D434)、等宽大写 A(U+1D670)、等宽数字 0(U+1D7F6)、双线大写 A(U+1D538)、双线数字 0(U+1D7D8)、全角大写 A(U+FF21)、全角数字 0(U+FF10)——7 个全部被折叠回普通 ASCII 字符。你看到"变好看了",对方看到的可能还是普通字母,这个差异你这一端无法预测。 - 显示成空心方框是字体缺字形,不是工具出错。排版时先在你当前字体里找这个码位有没有字形,找不到就沿 fallback 字体链往下找,整条链都没有才画一个
□(正式名叫.notdef)。数学字母区块覆盖面本来就不完整,同一段文字在不同手机上可能一部分正常、一部分是方框。方框取决于对方设备,你无法替他解决。 - 不要把结果当密码、网址或账号名。这类字符在各地的归一化行为不一致,会导致"我明明输对了却登录不上";同时它们不在普通字母表里,多数校验规则会直接报"含非法字符"。
- 码点是定值,字形不是。页面列出的码点来自 Unicode 的分配表,任何设备上都一样;但"看起来像不像手写"完全取决于字体,同一串在 A 设备上像连笔斜体、在 B 设备上可能只是一排普通字母加几个方框。
常见问题
为什么不能用 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 面板边输入边观察,看不到新请求发出。