字帖生成器 v3.0.7 · 离线笔画笔顺动态演示(双图层重构 + 暂停/恢复 + 触屏/平板自适应 + 可视视口精确居中)
← 返回字帖主页新增v2.9.8 起字帖支持离线汉字笔画笔顺动态演示。点击字帖中任意汉字方格,即弹出标准窗口式演示弹窗,逐笔演示笔顺动画。
"笔顺演示"控件位于字体下拉菜单下方的快捷工具栏,与"刷新""打印"按钮平齐:
btn-primary 橙色高亮(默认状态)btn-secondary 灰色暗淡弹窗中央的方形区域为演示舞台,背景始终为白色(即使 dark 模式也保持白色,确保黑色笔画清晰可读)。
↑ 未点击"播放"时:汉字以 0.25 不透明度黑色轮廓显示
v2.9.8 播放按钮支持播放/暂停两态切换:
v2.9.8 采用双图层架构,确保 0.25 不透明度的汉字轮廓在演示过程中始终显示(位于底层,被笔画遮挡而非消失):
.sd-stage-bg):始终以 0.25 不透明度显示完整汉字(黑色 + 偏旁红色),不参与动画.sd-stage-fg):初始不可见,点击播放后逐笔绘制完全不透明笔画速度滑块位于播放按钮下方:
1x ~ 5x,步长 1x(v2.9.8 调整:5 档清晰可辨)1x(v2.9.8 调整:原 5x 过快,1x 更易观察笔画走向)| 元素 | 颜色 | 不透明度 |
|---|---|---|
| 普通笔画 | #000000(黑色) |
未播放 0.25 / 播放时 1.0 |
| 偏旁部首 | #d32f2f(朱砂红) |
未播放 0.25 / 播放时 1.0 |
偏旁部首的笔画索引由数据中的 radStrokes 字段自动识别。如该字段缺失,则所有笔画统一为黑色。
| 操作 | 方式 |
|---|---|
| 最小化 | 点击标题栏右侧 ▱ 按钮,隐藏主体仅保留标题栏;同时记录当前窗口位置/尺寸 |
| 最大化 | 点击标题栏右侧 ▢ 按钮:从最小化恢复时精确还原最小化前的位置与尺寸;非最小化状态下切换最大化(放大/还原) |
| 关闭 | 点击标题栏右侧 ✕ 按钮,或按 Esc 键关闭最上层弹窗 |
| 拖拽 | 按住标题栏拖动(最大化状态不可拖拽);拖拽范围限制在屏幕内,窗口不会被拖出屏幕外(v3.0.4) |
| 复位排列 | 双击标题栏,把自由拖拽的窗口复位回自动排列(v3.0.4) |
| 聚焦 | 点击相同汉字的字格,已有弹窗会置顶并闪烁高亮(v3.0.4 补齐闪烁样式,触摸端点按有可见反馈) |
MAX_WINDOWS = 4)v3.0.4 重构了弹窗的排列与触屏交互,桌面端行为刻意保持不变:
solveLayout(n, availW, availH, sMax) —— 对每种列数计算缩放 s 并取最大值,多弹窗自动排成网格并整体缩放以适配屏幕sMax = 1.0,只要 1×N 横排放得下,s 恒为 1,与旧版逐像素一致1.6,单个弹窗可放大到 1.6 倍,充分利用平板屏幕s 低于可读下限 0.72 时,自动把最旧的展开弹窗收为最小化药丸,直到放得下或只剩一个;药丸可点击恢复(按 LRU 顺序)26 → 34px、播放按钮 36 → 44px、速度滑块拇指 14 → 22px(并按当前缩放反向补偿,保证实际触摸尺寸达标)v3.0.6 修掉了 pad / 手机上两类长期存在的排布缺陷。根因不在 CSS 写得对不对,而在"以哪个视口为基准":
position:fixed + left/top:50% 的百分比基准是布局视口(初始包含块),而用户真正看到的是 visualViewport(可视视口)。二者在这些场景并不重合——软键盘弹出(可视高度骤减、布局视口不变 → 弹窗中心落到键盘后面)、双指缩放后平移(offsetLeft/offsetTop 非 0)、移动端地址栏收放、无头浏览器 isMobile 模拟。这正是"单窗口不在正中央"的成因visualViewport 计算可用区,并无条件把 overlay 锚到可视视口正中心(left = offsetLeft + width/2,top 同理)。之所以不做"发现跑出去了才纠偏":诉求是总在正中央,"在可视区内但偏心"同样是缺陷。该式在两视口重合时与 CSS 基线逐像素等价(桌面 left:50% = clientWidth/2 = visualViewport.width/2 + offsetLeft(0)),因此桌面端零回归max-width/max-height 与"最大化"尺寸从 100vw/100vh 改为 --sd-avail-w/--sd-avail-h(由 JS 按可视视口写入)。移动端的 100vh 是"大视口"高度,比真正可见的区域高出一截grid-column: 1 / -1 + justify-self: center —— 这是精确居中;仅靠整数列起点做不到(列起点只能对齐整条轨道,奇数条目放进偶数列必然偏半格)s 向下取整到 1e-4,宁可略小也不因浮点误差溢出可用区实测取证(scripts/measure-popup-layout.cjs,只读测量、不改产品代码):7 种设备(桌面 1920×1080 / 笔记本 1280×800 / 平板横竖 1180×820、820×1180 / 手机 390×844、844×390、360×640)× 1–4 窗口,另加 2× 页面缩放场景(用 CDP Emulation.setDeviceMetricsOverride 造出稳定的"布局视口 ≠ 可视视口"稳态),并在同一页面里做 A/B 对照:
| 场景 | 修复后偏差 | 纯 CSS 方案(对照列)偏差 |
|---|---|---|
| 网格排列(28 组:7 设备 × 1–4 窗口) | 最大 0.1px(几何稳定 28/28,可疑 0) | — |
| 2× 页面缩放(10 组) | 0, 0(可视视口确已分离 10/10,几何稳定 10/10,可疑 0) | 最大 387.8px 横向(phone-360x640)/ 806.8px 纵向(phone-390x844) |
left/top 清掉、退回旧的纯 CSS left/top:50% 后当场量出来的,不是推算值。它的数值随文档高度浮动(布局视口高度取决于当时内容高度),所以它是一个量级证据——说明不修就会偏出几百像素;而「修复后 = 0」那一列才是逐次可复现的判据。
top 已经是正确值,但窗口还在插槽内做入场收尾,包围盒中心偏 7.6px,再等 1500ms 自行回到 0。也就是产品没错、关口采早了。已把判据抬到连续 6 次(600ms 静止)。MEASURE_STABLE_TIMEOUT=1 会连带抹掉轮询等待,于是开/关窗节奏被拉满,Chromium 抛 detached Frame。退出码确实是 1,但那是「崩了」而不是「判据咬人了」,负控等于失效。已加固定沉降 MEASURE_SETTLE_MS=700 与轮询解耦;修完实测 detached Frame 0 次、28/28 + 10/10 如实判为 UNSTABLE、退出码 1。page.evaluate 只序列化传入的函数本身,于是每次都抛 ReferenceError 又被 catch 吞掉,伪装成"几何不稳定",整份报告 28/28 全红且原因不可见。判据自己也要能被证伪;而「退出码非 0」不等于「负控生效」,必须看输出差分。
笔画数据来源于 MakeMeAHanzi 数据集,本项目内置 9574 个汉字的完整笔画信息(SVG 路径 + 中心线 + 偏旁索引)。
每个汉字的数据结构:
{
"strokes": ["M 518 382 Q 572 385 ...", ...], // SVG path d 字符串数组,长度即笔画数
"medians": [[[121, 393], [193, 372], ...], ...], // 每笔中心线坐标
"radStrokes": [0, 1, 2] // 可选:偏旁笔画索引
}
| 方案 | 文件 | 大小 | 说明 |
|---|---|---|---|
| 主数据源 | /hanzi-data/hanzi-data.bin |
11.75 MB | Gzip 二进制,Web Worker 后台解压 |
| 降级备用 | /hanzi-data/hanzi-data-embedded.js |
15.7 MB | Base64 + Gzip,file:// 协议时使用 |
当点击字格查询汉字数据时,按以下顺序查找:
_customDataMap)— 用户手动添加的冷僻字 / 新造字,优先级最高_charDataMap)— 9574 汉字离线字典_networkCache)— 之前从 CDN 加载过的字符(内存缓存)navigator.onLine === true 时)— 从 jsdelivr CDN 在线获取,成功后缓存到 _networkCache自定义数据通过 localStorage 持久化(key: calligraphy_custom_hanzi):
setCharData + persistCustomData 添加冷僻字clearCustomData 清空自定义数据persistNetworkCache 将网络缓存的冷僻字合并到自定义数据并持久化clearNetworkCache 仅清空内存中的网络缓存支持运行时通过代码插入冷僻字 / 新造字数据:
import { setCharData, persistCustomData, buildCharData } from './src/modules/hanziDataStore.js';
// 构造规范化数据(自动校验)
const data = buildCharData(
["M 100 500 L 924 500"], // SVG path d 字符串数组
[[[100, 500], [924, 500]]], // 每笔中心线坐标
undefined // 可选:偏旁笔画索引
);
// 插入并持久化
setCharData('㐀', data);
persistCustomData();
坐标系必须为 MakeMeAHanzi 规范(X 轴 0 ~ 1024,Y 轴 -124 ~ 900,向下为正),否则渲染会错位。
| 按键 | 作用 |
|---|---|
| Esc | 关闭最上层演示弹窗 |
hanzi-writer npm 包(^3.5.0),通过自定义 charDataLoader 注入离线数据,脱离 CDN 依赖fflate 纯 JS 库的 gunzipSync 解压 Gzip 二进制.bin fetch 失败 → .js 备用viewBox + preserveAspectRatio="xMidYMid meet" 实现弹窗大小变化时字体始终居中充满详细数据结构与压缩方案见 离线汉字数据方案_v2.9.8.md。
请检查工具栏"笔顺演示"按钮是否处于开启状态(橙色高亮)。若关闭则点击字格不会弹出弹窗。
该字不在 9574 内置汉字范围内,且当前处于离线状态或网络备选加载失败。请联网后重试,或通过 setCharData 接口手动添加该字数据。
舞台背景始终为白色,确保黑色笔画在任何主题下都清晰可读。dark 模式下仅弹窗边框、标题栏、控件区跟随主题变化。
这是设计要求:未播放时整个汉字以 0.25 不透明度的黑色轮廓显示,作为预览。点击"▶ 播放"后切换为完全不透明逐笔演示。
依次点击不同汉字的字格,最多同时弹出 4 个弹窗。相同汉字的字格点击不会新开弹窗,而是聚焦已有弹窗(置顶 + 闪烁高亮,需手动点击"▶ 播放"才会演示)。
| 版本 | 日期 | 主要变更 |
|---|---|---|
v2.9.8 |
2026-08-05 | 新增离线笔画笔顺演示弹窗、工具栏按钮、9574 字离线数据 |
v2.9.8 |
2026-08-05 | 按钮文字化、自动播放移除、导航介绍联动抽屉、独立介绍页面 |
v2.9.8 |
2026-08-05 | 双图层重构(0.25 轮廓始终显示,仅被笔画遮挡);默认速度 5x → 1x;修复最小化/最大化按钮失效;最小化→最大化精确还原位置与尺寸;弹窗固定宽高比 |
v2.9.8 |
2026-08-05 | 修复速度滑块失效(hanzi-writer 3.7.3 无 updateOptions);滑块改为 5 档(1x-5x);修复移动端 light 主题;修复打印黑框 |
v2.9.8 |
2026-08-06 | 播放速度持久化到 localStorage,后续新弹窗默认采用上次调整的速度 |
v2.9.8 |
2026-08-06 | 播放按钮支持播放/暂停两态切换(pauseAnimation/resumeAnimation);播放中按钮变粉色提示可暂停 |
v3.0.0 |
2026-08-06 | 笔画弹窗加载态+就绪徽章;AI组词补齐(双引擎:DeepSeek/火山引擎豆包);三项独立开关(全量检查/组词补齐/拼音纠错)+旋转图标进度提示 |
v3.0.4 |
2026-09-18 | 触屏/平板自适应重构:容器改 CSS Grid + solveLayout 求解器自动排列与缩放;手机空间不足时自动收最旧窗口为药丸(LRU 恢复);触摸目标放大(按钮 26→34px、播放 36→44px、滑块 14→22px);拖拽视口边界约束 + 双击标题栏复位;补齐聚焦闪烁样式;新增 769–1280px 平板断点;桌面端布局零退化 |