sketch-infographic
目标
把用户想说明的内容转成清晰的信息图,而不是套用固定业务模板。产物应当:
- 讲清关系或形态:读者先看到结论和主结构;需要时还能看出体积、方向、包含与变换。
- 可维护:图形定义、SVG 都是文本,可重渲染、可 diff。
- 可移植:不写死用户路径、平台字体、业务术语或私有服务。
- 可验证:运行语义几何 lint、SVG 产物 lint,并实际查看最终图片。
- 可组合:能力边界由通用图形原语决定,而不是由模板、案例或领域清单决定。
- 可扩展:画布尺寸、语言、颜色、图式、Lucide 图标与自定义
svgGlyph均按任务选择。
核心 SVG 渲染无 npm 运行时依赖,只使用 Node 标准库。完整 Lucide 图标数据已经固化在 skill 内,正常出图不访问网络。PNG 是可选增强,依赖本机浏览器;找不到浏览器时仍应交付 SVG。 实际显示效果可能因本机字体而略有差异。
工作流
1. 理解内容和交付环境
先从用户材料中提取:
- 图要表达的核心结论;
- 读者是谁,以及图片放在哪里;
- 必须出现的节点、关系、分组、形态要素(尺寸消长、通道深浅、包含关系等)和强调项;
- 语言、尺寸、横竖版、背景和输出格式要求。
信息量不足时,先确认信息密度(再落笔)。 若用户在绘图前只给了题目/一句话主题、极少节点,或未说明要
概括总览还是展开细节,不要自行假设疏密。应调用 AskUserQuestion(或当前 Agent 等价的结构化
提问工具)询问本图期望的信息量程度,并据此控制图中信息密度。勿用普通闲聊式追问代替该工具。
建议选项(可按语境微调文案,但保持三档清晰):
| 档位 | 含义 | 落笔约束 |
|---|---|---|
| 低密度 | 一眼总览:少节点、短标签、主结构优先 | 3~7 个主元素;少副标题与注解;不塞细节流程 |
| 中密度 | 讲清关系:关键节点与一级分支齐全 | 分组清晰;每块 1 行说明即可;避免二级展开墙 |
| 高密度 | 尽量完整:步骤、分支、标注、例外都要可见 | 可加大画布或拆总览+局部图;仍保持可读,不靠缩字硬塞 |
用户已明确密度、交付媒介(如「封面示意图」「一页讲透」),或材料本身已足够支撑疏密判断时,可跳过 此问,直接选型绘制。
用户没指定尺寸时,根据媒介选择合理比例:默认 1200 × 675;文档长图可增加高度,社交媒体
或头像式卡片可用正方形,移动端可用竖版。形态密集的教学/结构图常需要更大画布
(如 1400 × 800)。不要因为默认值而强行使用横向长方形。高密度内容优先加高/拆图,而不是压字号。
2. 选择视觉语法(先选型,再落笔)
按信息关系选结构,下面只是常见起点,不是封闭清单:
| 读者主要要看 | 优先视觉语法 | 常用原语 |
|---|---|---|
| 节点名与依赖 | 流程、拓扑、泳道 | card / box + connect / bypass |
| 分类与层级 | 树、分层、嵌套边界 | 区域 box + 内部小块 |
| 比较与取舍 | 左右对比、矩阵、象限 | 同构两列 + 差异变色 |
| 时间与变化 | 时间线、阶段、闭环 | 主轴 + 刻度 / curve |
| 形态与变换 | 图形化示意 | circle / ellipse / 叠层 rect / arc / path / polygon / svgGlyph |
也常见:因果链、研究框架、中心辐射、数据流。优先一个主结构,允许组合。
默认不要先画一排等宽卡片。 先问一句:
读者需要记住的是「叫什么」,还是「长什么样、如何变」?
- 叫什么 →
R+card/box+connect/bypass(拓扑与流程)。 - 长什么样 → 用几何体积说话:叠层块变窄变厚、圆点阵列、扇形/柱状得分、自由
path轮廓、局部放大窗;文字只做短标注。参考examples/cnn-figures.mjs与光合作用叶片示意。
没有现成模板时,组合基础原语构造新图式;重复出现的能力再抽成与领域无关的公共 API。 内容过密时,拆成总览与局部图,比压缩字号或制造大量交叉线更清楚。
3. 决定代码放置方式
默认优先在临时目录或当前任务目录出图。只有用户明确要求长期维护、提交到项目仓库时, 才初始化自包含目录:
node <SKILL_DIR>/scripts/init.mjs <target-dir> --name figures
它会复制绘图库、完整图标数据、双层 lint、CLI 和单图种子。除非用户允许,不要覆盖已有文件;
需要覆盖时显式使用 --force。
一次性任务可以直接在临时目录写图形文件,并调用 skill 内的渲染器。JS import 中不能直接写
shell 变量 $SKILL;应使用真实的相对路径或绝对路径。
4. 编写图形定义
一张图对应一个构建函数,返回 createSketch() 画布,并通过 FIGURES 导出。
默认不要调用 s.footer()。 底部图注线 + 说明文字通常多余:结论应写在标题与图内标注里。
只有用户明确要求图注、脚注或「图下说明」时才加 footer。不要为了「显得完整」而自动补一段总结。
不加 footer 时,画布高度也不要为其预留空白——高度贴齐最低内容再留约 40~60px 边距即可;否则底部会显得空。
用字号区分信息层次。 不要整张图共用默认 size: 18。落笔前先定 2~4 级文字角色,再给
text / card / inBox / 连线 label 选不同 size(必要时配 weight: 700 或 GRAY),让读者
一眼分清主标题、分区标题、节点名、注解。示意阶梯(可按画布缩放微调,但要保持相对落差):
| 角色 | 建议 size |
典型写法 |
|---|---|---|
| 主标题 | 28~31 |
s.header 或 s.text(..., { size: 31, weight: 700 }) |
| 分区 / 组标题 | 20~22 |
区域角标、列标题 |
| 节点名 / 卡片正文 | 16~18 |
s.card / s.inBox 的默认档 |
| 注解 / 连线标签 | 12~14 |
wireLabel、connect 的 labelSize |
框太窄时 fit 会悄悄缩字,破坏层次;关键层级用 fit: false,或先 measure 留够宽度。
过密时加高/拆图,不要靠全局压字号来「塞进」内容。细节见 references/layout.md 与 api.md 文字节。
拓扑/流程示例:
import { createSketch, R, INK, GREEN, VIOLET, GRAY } from './sketch.mjs';
function overview() {
const s = createSketch({ width: 1200, height: 675 });
s.header({ title: '标题直接表达结论', sub: '副标题补充范围或条件' });
// 长英文标题会自动折行并下移正文起点;用返回的 y 排内容,勿写死 96/128
const input = R(90, 250, 180, 70, 'input');
const core = R(420, 220, 300, 130, 'core');
s.card(input.x, input.y, input.w, input.h, '输入', { stroke: GREEN });
s.card(core.x, core.y, core.w, core.h, '核心处理', { stroke: VIOLET });
s.connect(input, 'e', core, 'w', { id: 'input-to-core', stroke: GRAY });
return s;
}
图形化示意示例(形态优先,外层仍用 R + track 供 lint):
function morph() {
const s = createSketch({ width: 1200, height: 700, stroke: { roughness: 1.3 } });
s.header({ title: '用体积变化讲清变换', sub: '叠层变窄变厚,而不是六个标题卡片' });
const stack = R(400, 220, 90, 120, 'stack');
for (let i = 3; i >= 0; i -= 1) {
s.rect(stack.x + i * 5, stack.y - i * 4, stack.w, stack.h, {
stroke: VIOLET, fill: i === 0 ? VIOLET : undefined,
fillStyle: i === 0 ? 'hachure' : undefined, lint: false,
});
}
s.track(stack);
const n1 = R(600, 250, 24, 24, 'n1');
s.circle(612, 262, 24, { stroke: GREEN, fill: YELLOW, fillStyle: 'dots' });
s.track(n1);
s.curve(stack.x + stack.w, 280, 560, 260, 600, 262, {
stroke: GRAY, head: 9,
});
return s;
}
export const FIGURES = [['overview', overview], ['morph', morph]];
按需读取:
references/api.md:画布、图形、文字(含size分层)、填充、连接、完整 Lucide 与 lint API;references/layout.md:布局、留白、字号分层、方框 vs 图形化选型、复杂连线;assets/template-figures.mjs:init落地用的单图种子(不是图式清单);- 仓库
examples/:跨领域展示与回归样本(含cnn-figures.mjs形态示意)。
如果任务不属于任何示例,仍然继续设计。优先用已有通用原语完成;确实缺少表达能力时, 扩展几何、文字、样式、布局或导出层,而不是新增只服务于某个行业名词的专用函数。
5. 绘制复杂形态信息图
当用户要「结构图 / 剖面 / 特征图 / 长什么样」时,按下面做,避免退化成方框标题墙:
- 先定视觉隐喻:体积消长、包含嵌套、阵列汇聚、滑窗扫描、轮廓剖面等,选 1~2 个。
- 用几何编码信息:
- 宽高变化 → 空间分辨率;
- 叠层厚度/层数 → 通道或深度;
- 圆点列 → 神经元 / 样本;
- 扇形或柱高 → 概率或得分;
path/svgGlyph→ 领域轮廓(叶、细胞、外壳),不要为此加行业专用 API。- 轮廓本身就是画面主体时,按下面「先写几何函数」那一节做,不要手写控制点。
- 装饰与结构分离:内部网格、示意剖面线、小核窗口等设
lint: false;外轮廓用R+track(或connect的端点)登记,保证重叠/穿线仍可查。 - 故意重叠要声明:滑窗叠在输入图上时,对内层
track(..., { allowOverlap: true })。 - 连线服务形态:主干可用
connect;扇入、扫描、回流用curve/polyArrow,并带{ id, from, to };密集交叉处抽稀连线或allowCrossing: true。 - 手绘感可略加强:
createSketch({ stroke: { roughness: 1.3, bowing: 1.15 } }); 正式文档可降到0.8+FONT_SANS。 - 标注短而靠边:名称放在形态下方或外侧,不要把长句塞进每个小形状中心。
复杂形态图同样必须 --lint-strict 并通过肉眼检查;手绘抖动不是重叠的借口。
形态当主体:先写几何函数,再推导内部构件
当画面主体是一个自由轮廓(叶片、细胞、器官、装置剖面、地层、地形),不要手写贝塞尔 控制点去凑形状,也不要把内部构件的坐标一个个写死。按三步来:
- 用参数化函数描述轮廓:沿主轴取归一化参数
t,写出该处的半宽(或半径、厚度)。 - 轮廓由采样点平滑连成,不手写控制点。
- 所有内部构件都从这个函数推导:细胞器、通道、开口、附着点、标注落点,一律用 「该高度的宽度 × 比例」定位,而不是各写一个绝对坐标。
// 1. 轮廓:t^A * (1-t)^B,两端自然收尖;A、B 控制哪头更尖
const A = 1.15;
const B = 1.0;
const raw = (t) => (t <= 0 || t >= 1 ? 0 : t ** A * (1 - t) ** B);
const PEAK = (() => { let m = 0; for (let i = 0; i <= 400; i += 1) m = Math.max(m, raw(i / 400)); return m; })();
const yAt = (t) => TIP_Y + t * LEN;
const halfAt = (t) => (MAX_HALF * raw(t)) / PEAK; // 该高度的半宽
const edge = (t, side) => [CX + side * halfAt(t), yAt(t)];
const inner = (t, frac) => [CX + frac * halfAt(t), yAt(t)]; // 主体内的点,必然在轮廓里
// 2. 采样 + 平滑成 path
const right = []; const left = [];
for (let i = 0; i <= 30; i += 1) {
const t = i / 30;
right.push(edge(t, 1)); left.push(edge(t, -1));
}
s.path(smoothPath([...right, ...left.reverse()], true), { stroke: GREEN, sw: 2.4, roughness: 0.8 });
// 3. 内部构件按比例落位,改 A/B 时会自动跟着走
drawOrganelle(s, ...inner(0.5, 0), 1); // 主轴上
drawOpening(s, ...inner(0.66, -0.84)); // 贴近左缘
smoothPath 把采样点两两取中点做二次贝塞尔,几行即可:
function smoothPath(pts, close = false) {
const f = (n) => n.toFixed(1);
let d = `M ${f(pts[0][0])} ${f(pts[0][1])}`;
for (let i = 1; i < pts.length - 1; i += 1) {
const [x, y] = pts[i]; const [nx, ny] = pts[i + 1];
d += ` Q ${f(x)} ${f(y)}, ${f((x + nx) / 2)} ${f((y + ny) / 2)}`;
}
const last = pts[pts.length - 1];
d += ` L ${f(last[0])} ${f(last[1])}`;
return close ? `${d} Z` : d;
}
这样做的收益是改一个参数,整张形态和所有内部构件一起变:叶子要更瘦就调 A,
要更大就调 MAX_HALF,不必重排任何一个器官或标注。
三个必须避开的坑:
| 坑 | 后果 | 修法 |
|---|---|---|
| 手写控制点凑轮廓 | 形状不可控,改一处要重调一串数字 | 换成参数化半宽函数 |
| 用起点处的宽度算构件终点 | 在轮廓收窄处戳出边界(叶脉穿出叶缘) | 用终点那个位置的宽度,跨度大时取 Math.min(halfAt(t0), halfAt(t1)) |
| 底层纹理透过内部器官 | 器官上叠着主脉/网格,看不清 | 先 PAPER 不透明填充盖一层,再补回底色,最后画外膜 |
lint 管不到这一层。 轮廓和装饰通常设 lint: false,所以形状画歪、器官戳出边界时
lint 照样全绿。参数化形态图必须真的看图;发现不对优先改函数参数,而不是去挪单个坐标。
6. 使用通用视觉资产
- 优先用
s.lucideIcon('<official-name>', ...)或s.iconLabel(...);当前固定版本提供 全部官方图标,可通过s.lucideIconNames查询。 - 图标名称使用 Lucide 官方 kebab-case,不要维护场景白名单。
- Lucide 表达不了的示意形(叶子、细胞、装置剖面、品牌符号等)用
s.svgGlyph(...)传入与 Lucide 相同格式的元素数组,或用绝对坐标s.path(...)/polygon组合;不要为 某个行业单独加专用 helper。 - 图标会自动登记包围盒并参与语义 lint:图标互压(
ICON_OVERLAP)、图标压住节点 (ICON_NODE_OVERLAP)、连线穿过图标(EDGE_THROUGH_ICON)。 - 图标画在卡片内部时,传
parent: box.id(与卡片同一id),避免把「图标属于该节点」 误报为重叠;不要把图标放在卡片顶边正中——北向端口和上方连线很容易穿过它。 - 装饰性底图(如教学图的叶子轮廓)可设
lint: false,避免与 region / 连线误报。 - 颜色用于表达语义,不用于装饰;同组图片保持一致。
- 图中文字应简洁,但不要机械限制字数。通过换行、扩大卡片或拆图保证可读性。
- 字号要有层次:主标题 > 分区标题 > 节点名 > 注解/连线标签;用
size/labelSize显式配置,避免全图同号。层次靠字号与字重拉开,不要只靠颜色。 - 用户已有品牌色、术语、语言和视觉规范时,优先遵循用户规范。
7. 复杂拓扑使用结构化几何
节点和连线一多(尤其是方框架构图),就不要手写框心箭头:
- 节点统一用
R(x, y, w, h, id)描述,绘制和连线复用同一对象; - 主干用
connect()贴边连接; - 回流、跨层依赖用
bypass()占用外围走线槽;终点在走廊平行边时自动approach退让,起点不额外折弯; 若左右绕行,出发侧宜用w/e与via同向,避免从n/s出发再横穿顶边; - 手写
polyArrow()时提供{ id, from, to }; - 普通线若参与检查,用
trackRoute()登记; - 独立区域用
track();容器标记为kind: 'region'; - 线旁文字用
wireLabel(),避免和边界融合。
共享总线可以对具体边声明 allowOverlap: true。只有视觉上已经明确处理的必要交叉才使用
allowCrossing: true;不要为了消除报告而全局关闭检查。EDGE_THROUGH_NODE 也会报贴边擦线
(净空默认 10px);起终点仅在端口邻域内允许贴近。
形态图与拓扑图可以在同一画布组合:左侧图形化主干 + 右侧标签卡片说明。
8. 渲染、检查、迭代
node render.mjs figures.mjs --lint-strict
node render.mjs figures.mjs --png
node render.mjs figures.mjs --only overview --lint-strict
--lint 同时执行两层辅助检查(不是完整布局证明):
- 语义几何 lint:已登记节点/图标/连线的区域重叠、线穿节点或图标、贴边擦线(净空不足)、 线线交叉/共线、越界;
- SVG 产物 lint:非法数值、尺寸/viewBox 不一致、最终笔触和文字越界。
lucideIcon / iconLabel 默认登记;未登记的普通 line/box/card 不会自动进入语义层。
lint 通过不代表视觉一定正确。打开 SVG 或 PNG,按顺序确认:
- 缩略图下主结构、阅读方向、形态隐喻是否一眼成立;
- 文字与图标是否可读,有无截断、压线、图标互压或压住卡片文字;主次字号是否分明
(标题明显大于节点,注解明显小于正文,且未被
fit压成同一档); - 顺着每条箭头能否确认起点、终点和方向,且未穿过图标;
- 分组、留白、对齐和颜色语义是否一致;
- 放进最终媒介后是否仍然清晰。
发现问题就调整坐标、尺寸或结构并重渲染。不要把手绘风当作重叠和歧义的理由。
9. 交付
默认保留图形源文件和 SVG;用户需要兼容办公软件、社交媒体或演示文稿时再导出 PNG。 交付时简要说明:
- 生成了哪些文件;
- 使用了什么尺寸和格式;
- 采用的是拓扑图式还是图形化示意(或二者组合);
- lint 是否通过;
- PNG 若未生成,是因为缺少浏览器还是用户未请求。
不要提交临时下载、试验依赖、浏览器缓存或无关对比产物。
维护与开源约束
这是面向所有用户的通用开源 skill。修改时遵守:
- 新能力应扩展可表达的几何、排版、样式、连接、数据映射或导出能力,不绑定某个行业、 公司、语言或当前案例;
- 不以模板数量、示例类型或内置组件名称定义能力范围;任何场景都可以组合原语形成新图式;
- 保持正常渲染零网络、零 npm 依赖;维护脚本可以联网,但必须固定版本并固化结果;
- 不复制维护第二份核心实现;示例应复用
scripts/下的正式代码; - 新增公共 API 时同步更新
references/api.md、init 种子和验证; - 更改布局或 lint 时用简单图、复杂拓扑图与形态示意样本同时回归;
- 保留第三方资源的版本、来源和许可证说明(见
lucide-icons.mjs头部注释)。
不适用场景
- 大规模数据、统计推断或交互筛选:优先使用专业图表库;简单数据图和解释性标注仍可作为 信息图的一部分绘制;
- 照片级插画、写实封面或自由绘画:优先使用图像生成或设计工具;本 skill 做的是可复现的 白板级图形化示意,不是写实插画;
- 用户明确要求 Mermaid、PlantUML、Figma 等其他可编辑格式:遵循用户要求;
- 一个简单文本列表比图更清楚:不要为了“有图”而画图。