wx Flowchart Workshop · 流程图工坊
一套可执行的流程图生产流水线:文字 → 图数据(JSON)→ 单文件网页。
不是模板拼接,是真的排版引擎:给节点和连线,它自己算层级、自己走线、自己避让。
> 作者:丁文轩 · 为小学信息科技「身边的算法」单元而做
开工前:先问,再画(硬性)
不许上来就生成。 用户只丢一句"画个流程图"或一张课本截图时,按这三步走:
第 1 步 · 问用途(决定形态)
直接执行 node scripts/brief.mjs --template,把四个问题发过去:
A 讲课板书:我演示、学生看
B 学生自己画:给图形库让他们拖
C 课堂练习 / 随堂检测:挖空 + 判分(最常用)
D 只要一张图:贴课件、试卷(导出 PNG)
E 闯关游戏:判分 + 干扰项 + 撒彩带
用途 → 配置:
| 用途 | mode |
挖空 | 干扰项 | 演示 |
|---|---|---|---|---|
| A 板书 | build |
无 | 无 | 要 |
| B 学生画 | build |
无 | 无 | 可选 |
| C 练习 | fill |
3~5,含判断条件 | 1~2 | 建议留 |
| D 只要图 | build |
无 | 无 | 不要 |
| E 闯关 | fill |
3~5 | 2~3 | 可选 |
第 2 步 · 先出一张预览图(必做)
不要用文字"念"流程图。 13 个框连编号带分支念下来,用户根本对不上号;
一张图贴过去,3 秒就能看出哪里不对。
node scripts/preview.mjs draft.json --out preview.png
- 只截流程图本身(自动加
?clean=1:隐藏工具栏、侧栏、状态栏、水印),图会放大到占满画面; - 一次 3 秒,比"改完代码再看"便宜得多;
- 预览图和最终产物同一套引擎渲染,所见即所得。
贴图的同时只配一两句话说改动的重点,不要复述整张图:
> 按你给的画了:一个总判断「起诉前是否必须先申请行政复议?」,是分三条支路各自收尾,否直落到"6 个月内起诉",最后全汇到"向人民法院提起诉讼"。
> 我暂时把三条支路竖排,要不要改成横排(A/B/C 并排)?
用户点头 → 再写正式 spec;说要改 → 改完再出一张图。
> 文字版只在两个地方用:① 用户看不了图(纯文字终端);② 只有 3~5 个框的小图。
> 超过 7 个框,只给图。
文字材料最常错的地方不是"画错"而是"切错"(两个动作塞一个框、并列写成顺序、漏掉"否则")。
预览图正是用来抓这个的。
第 3 步 · 只对 C/E 追问练习细节
- 挖哪几个?(建议 3~5 个,至少一个判断条件)
- 要不要干扰项?几个?(建议 1~2 个"看着对但位置不对"的)
- 判分要不要接受同义写法?要不要"看答案""演示"按钮?
用户不回答怎么办
别干等。按默认先做,交付时一句话交代:
没说用途 → 按 C 练习做:教材配色、3 个空(含判断条件)、2 个干扰项、留演示按钮。
详细版(含反例、干扰项三条经验、交付话术)见 reference/brief.md。
先看产出
| 形态 | mode |
学生做什么 | 典型场景 |
|---|---|---|---|
| 搭建 | build |
从图形库拖出图形、连线、双击写字 | 新授后让学生自己画一遍 |
| 补全 | fill |
把词块拖进空槽;槽里的还能再拖出来——拖到别的槽是搬家/交换,拖到空白或词库是放回待选区 | 课堂练习、随堂检测 |
| 演示 | 同上加按钮 | 点"演示",在判断处选"是/否"走一遍 | 讲清算法的执行路径 |
三种形态都是同一个引擎,只是 mode 和 quiz 不同。成品是一个 HTML 文件,双击即用,不联网。
配色标准(教材风,默认)
所有流程图默认用这套颜色,和课本插图一致:
| 元素 | 填充 | 描边 | 文字 |
|---|---|---|---|
| 开始 / 结束(药丸) | #c4ddbb |
#93c688 |
#2b4a24 |
| 输入 / 输出(平行四边形) | #d3effc |
#4fc3e8 |
#173d4d |
| 判断(菱形) | #ace0f9 |
#4fc3e8 |
#123544 |
| 处理(矩形) | #ffffff |
#4fc3e8 |
#173d4d |
| 连线 / 箭头 | — | #2aa9cf |
— |
其余三套只作备用,几何完全一样,换色不破版:
| 主题 | 用途 |
|---|---|
textbook(默认) |
教材配色,最"像书上印的" |
paper |
纯黑白线稿(课本是黑白印刷时用这个) |
candy |
浅彩色,低年级更活泼 |
chalk |
黑板风,适合暗色投影 |
切换:--theme paper,或页面右上「主题」按钮轮换。
> 画布右下角有一个竖向 WX 水印(26px、7.5% 透明度、固定尺寸不随缩放变化,pointer-events:none 不挡操作)。
> 它只在页面上,不进导出图。要让它跟着 PNG/SVG 一起出去,说一声——在 exportSvgString() 里加一段 <text> 即可。
视觉基准(别凭手感改)
比例是这套东西的命根子。参考图量出来的基准(字号 F = 12.5px 时):
| 项 | 值 | 为什么 |
|---|---|---|
| 线宽 | 1.5 | 原图 1px;再粗就像 PPT 剪贴画 |
| 节点高 | 30(≈2.4F) | 字占框高约 40%,看着像"写进去" |
| 菱形 | 宽 = 文字宽 + 30,高 = 宽 / 2.2 | 比常见网页库扁,接近课本 |
| 分支偏移 | 菱形半宽 + 20 | 跟着父节点宽度走,分支线才笔直 |
| 行距 | 15 | 原图 10~15px;松一倍就"散架" |
| 汇合额外间距 | +40 | 给左右并线留位置 |
| 箭头 | 9×8 实心三角 | 原图箭头偏大、线条偏细 |
改版前先做一件事:有参考图就先量,别凭眼睛估。
python scripts/measure.py 课本截图.png --out 量图报告.md
它给四样东西,都是写 spec 要用的:
| 输出 | 怎么用 |
|---|---|
| 页面底色 + 主色(含精确色值、占比、明度/饱和度) | 挑 3 个浅填充 → start/end、io、decision;挑最饱和的当描边 |
| 线宽中位数 | 线宽 ÷ 字号 = 倍数,写进引擎常量(课本一般 0.10~0.14) |
| 墨迹外接框 + 四周留白 | 判断图有没有居中、页边距多大 |
| 行间空白 | 空隙 ÷ 字号 ≈ ROW_GAP 的倍数 |
验证过的:拿课本那张「两个自然数比较」的扫描件跑,量出来的填充色是
#d3effc / #ace0f9 / #c4ddbb——和手工一个像素一个像素抠出来的完全一致。
> 注意:绝对像素没有意义(扫描件分辨率各不相同),只取比例。
> 依赖 Pillow(pip install pillow),没装也不影响生成页面。
环境要求(先看,省得踩坑)
| 项 | 要求 | 说明 |
|---|---|---|
| Node.js | ≥ 18 | 脚本用了 ESM 与 fs.rmSync;不需要 npm install,零依赖 |
| 浏览器 | Chrome 或 Edge | 只有 preview.mjs(截图)和 test.mjs(验收)需要;build.mjs / lint.mjs / brief.mjs 不需要 |
| 网络 | 不需要 | 产物与外链无关;装浏览器是唯一可能需要联网的步骤 |
| 系统 | Windows / macOS / Linux | 找浏览器顺序:CHROME_PATH 环境变量 → 各系统常见路径 → PATH 里的 google-chrome/chromium/msedge |
浏览器装了但不在默认位置时:
# Windows
set CHROME_PATH=D:\你的路径\chrome.exe
# macOS
export CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
# Linux
export CHROME_PATH=/usr/bin/chromium
找不到浏览器时,脚本会直接打印上面这几种写法,不会甩一堆堆栈。
30 秒上手
# 1) 看图数据长什么样(打印一份可直接改的模板)
node scripts/build.mjs --template
# 2) 写一份 JSON(照着 examples/ 改最快),打包成网页
node scripts/build.mjs examples/compare-two.json --out ../../output/两数比较.html
# 3) 用真浏览器验收(无头 Chrome,跑 16~21 项交互检查)
node scripts/test.mjs ../../output/两数比较.html --shots ../../output/shots
标准工作流(六步,别跳)
⓪ 问清楚
执行 node scripts/brief.mjs --template,问用途;把图念给用户确认;要挖空就问挖哪几个、有没有干扰项。
见上一节。没确认结构之前不要写 JSON。
① 读文字,先把"动作"抠出来
把材料按动作切成短句,一句一个动作。去掉"然后、接着、首先"——顺序由箭头表达,不靠文字。
原文:先输入开关指令,然后看看红灯开关是不是开着,如果开着就点亮红灯;
如果没开,再看绿灯开关,开着就点绿、没开就点黄,最后结束。
切分:输入开关指令 / 红灯开关是否开? / 红点亮 / 绿灯开关是否开? / 绿点亮 / 黄点亮 / 结束
② 给每句配图形
| 这句话在干什么 | 图形 | type |
|---|---|---|
| 流程的起点/终点 | 药丸 | start / end |
| 读取数据、显示结果 | 平行四边形 | io |
| 赋值、计算、执行动作 | 矩形 | process |
| 做判断、有两条出路 | 菱形 | decision |
判断框里只写条件本身,句尾加问号:写 红灯开关是否开?,不要写 如果……就……。
判断出来的两条线必须标 是 / 否(小学生不用 Y/N)。
③ 连边,标分支
主干一路向下;判断的两条出边默认"是→左、否→右"。想让"是"直接往下走,就给这条边写 "side": "down"(循环常用)。
多条分支汇合时只画一个汇合节点,不要每个分支都接一个"结束"。
④ 排版交给引擎
JSON 里不写坐标,引擎会:
- DFS 找回边(循环),先砍掉再分层——一圈循环只砍一条,不会把环拆散;
- 最长路径分层:一条链上的动作天然排在同一竖列;
- 按分支方向定水平位置:是往左、否往右,偏移量跟着菱形宽度走;
- 同层消重叠,单入口/单出口自动对齐(开始和结束在同一竖线上);
- 尺寸按字数自动算——改字不会破版。
只有特殊构图才在节点上写 cx/cy 钉死位置。
⑤ 静态检查 + 浏览器验收
node scripts/lint.mjs # 不可见字符 / 绝对路径 / 外链
node scripts/test.mjs <产物.html> --shots <截图目录>
lint 查三样,发布前必跑:
- 不可见字符(零宽空格 U+200B、BOM、双向控制符)——安全平台会判成"可疑编码/混淆";
- 绝对路径(盘符开头的完整路径)——别人装了用不了;
- 外部资源引用——违背"离线可用"。
test 看三件事:
- 有没有
✗:JS 报错、外链、重叠、拖拽失效都会被抓出来; - 打开
main.png:主干是否一条竖线、分支标签是否压线、有没有线穿节点; - 打开
demo.png:演示高亮是否落在正确的节点上。
不要凭感觉交付——肉眼看截图 10 秒,比读代码 10 分钟管用。
图数据(完整字段见 reference/spec.md)
{
"title": "判断两个自然数的大小",
"subtitle": "输入两个自然数,比较后输出较小的数",
"theme": "textbook",
"mode": "build",
"nodes": [
{ "id": "start", "type": "start", "text": "开始" },
{ "id": "d1", "type": "decision", "text": "a 小于 b 吗?" },
{ "id": "p1", "type": "io", "text": "输出 a 的值" }
],
"edges": [
{ "from": "start", "to": "d1" },
{ "from": "d1", "to": "p1", "label": "是" }
]
}
补全模式再加一段:
"quiz": { "distractors": ["a 大于 b 吗?", "输出 a 和 b 的值"] },
// 节点上加: "blank": true, "answers": ["红点亮", "红灯亮"]
文字 → 图 的判断规则
完整版在 reference/text-to-graph.md,最常用的几条:
- 一句话一个框,超过 10 个汉字就拆;框里写"做什么",不写"为什么"。
- 开始/结束各一个。出现第二个"结束"通常说明该用汇合节点。
- "如果……就……否则……"是判断;"重复……直到……"是循环,画成判断在前的当型循环。
- 并列的多个判断是"级联"(else-if 链),不是一个菱形派生出多个角。
- 不要有孤岛:每个节点都要能从"开始"走到。
| 症状 | 病因 | 改法 |
|---|---|---|
| 菱形里写了半句话 | 把整句塞进判断框 | 只留条件,加问号 |
| 两个"结束" | 每个分支各接一个 | 合并成一个汇合节点 |
| 一条线穿过别的框 | 手写坐标 / 分支方向反了 | 删掉 cx/cy,给边加 side |
| 循环画成一团 | 回边连到上一步而不是判断框 | 回边指向判断框,走外侧车道 |
| 一框分三岔,岔线像"相邻框互连" | 三条出边都从底部走,横移发生在本行高度 | 给三条边分别写 side: left / down / right;还不行就全图钉 cx/cy 手工摆 |
| 写了坐标但没生效 | 坐标是"全有或全无":缺一个就当整张图没写 | 每个节点都补上 cx 和 cy,或全删 |
补全模式:挖空与操作
操作(全部是拖拽,没有弹窗):
- 左边词块 → 拖进空槽;
- 槽里的词 → 拖到另一个槽:搬家(目标有词则交换);
- 槽里的词 → 拖到空白 / 拖回词库:放回待选区;
- 顶掉别人的词不会丢:被顶掉的自动回词库。
挖空原则:3~5 个空,包含至少一个判断条件;干扰项 1~2 个,优先放"看起来对但位置不对"的节点
(比如把"红灯开关是否开?"放进绿灯分支)。主干节点留着当脚手架。
课堂落地建议
- 投影分辨率 1280×800 起,教室大屏拉到 1600 宽再点「适应」。
- 平板/触屏:引擎用 pointer events,能拖;手指容易误触,建议先用大屏演示一遍手势。
- 交作业:导出 PNG,文件名带姓名;要收可编辑成果就导 JSON。
- 一屏放不下就拆图:主干一页、某个分支展开一页。
排错
| 现象 | 原因 | 处理 |
|---|---|---|
| 打开是空白页 | HTML 被截断 | 重新 build,检查引擎里有没有 </script> |
| 连线绕得很远 | 这条边被判成回边 | 检查是否真成环;不成环就调 side |
| 两个节点叠在一起 | 手写了相同的 cx/cy |
删坐标让引擎排,或点「整理」 |
| 导出 PNG 空白 | 浏览器拦了 canvas | 改导 SVG |
| 中文变方块 | 系统缺字体 | 引擎已指定微软雅黑/苹方,装「思源黑体」 |
文件地图
skill/wx-flowchart-workshop/
├── SKILL.md ← 你正在读
├── README.md ← 给"打开文件夹的人"看的快速上手
├── assets/
│ ├── studio.html ← 页面骨架(三个占位符)
│ ├── flow-engine.css ← 样式与主题(含 textbook 配色)
│ └── flow-engine.js ← 排版/路由/交互/导出 全部逻辑
├── scripts/
│ ├── brief.mjs ← 开工前四问 + 图数据自检(--template / --check)
│ ├── preview.mjs ← 先出预览图(?clean=1 只截流程图)
│ ├── lint.mjs ← 发布前静态检查(不可见字符 / 绝对路径 / 外链)
│ ├── build.mjs ← JSON + 骨架 → 单文件网页
│ ├── browser.mjs ← 找本机浏览器(CHROME_PATH / 各系统路径 / PATH)
│ ├── measure.py ← 量参考图:主色 / 线宽 / 留白 / 行距(需 Pillow)
│ └── test.mjs ← 无头浏览器验收(16~21 项)
├── examples/
│ ├── compare-two.json ← 课本拓展题:两数比较取较小(教材配色)
│ ├── compare-two-quiz.json ← 同一题的挖空版
│ ├── traffic-light.json ← 级联判断(红/绿/黄三盏灯)
│ ├── traffic-light-quiz.json
│ ├── loop-sum.json ← 循环结构(回边走外侧)
│ └── chicken-rabbit.json ← 从课件还原的算法(回边指回处理框)
└── reference/
├── brief.md ← 开工四问的完整版(反例、干扰项写法、交付话术)
├── spec.md ← 字段全表 + 校验规则
├── text-to-graph.md ← 文字→流程图 的完整规则与范例
├── casebook.md ← 教学目录里 11 个现成网页的拆解
└── engine.md ← 引擎原理、踩过的坑、二次开发入口
二次开发入口
页面挂了 window.__flow,控制台直接玩:
__flow.addNode('decision', 0, 300) // 在画布坐标 (0,300) 加判断框
__flow.addEdge('d1', 'p2') // 连线
__flow.tidy() // 重排
__flow.exportSVG() // 拿到 SVG 字符串
__flow.S // 全部状态(nodes/edges/rank/view)
加一个新主题:在 PALETTE 里加一组颜色,CSS 里补 body[data-theme="xxx"] 的变量即可。