# Sketch Infographic

> 用无 npm 运行时依赖的 Node 脚本生成可复现、可编辑、可进 Git 的手绘风 SVG 信息图，并可选 导出 PNG。当用户需要用视觉方式解释结构、流程、关系、比较、层级、时间、决策、系统、概念， 或需要图形化示意形态/体积/缩放/包含（如神经网络特征图、细胞结构、装置剖面、地理示意）时， 优先使用本 skill。适用于学术研究、教学、技术文档、软件与业务架构、产品方案、工作汇报、 README、文章、社交媒体和演示文稿。触发语句包括“画图/配图/示意图/架构图/流程图/时间线/ 决策树/泳道/矩阵/结构图/剖面图/特征图/infographic/diagram/visualize”等，也用于修改已有图。 不要默认只画等宽方框流水线；先判断读者要看节点名还是形态变换，再选 card 拓扑或圆/叠层/ path 等图形化手段。不要把它局限于内置模板或当前案例。大规模统计图、照片级插画或用户 明确指定其他格式时，应选择更合适的工具。

- Skill: `morvanzhou/sketch-infographic` (Agent Skill, multi-file: 10 files)
- Install (CLI): `npx skillmds@latest add morvanzhou/sketch-infographic`
- Raw SKILL.md: https://api.skillmd.com/api/skills/morvanzhou/sketch-infographic/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: MorvanZhou (https://skillmd.com/u/morvanzhou)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/morvanzhou/sketch-infographic

---


# sketch-infographic

## 目标

把用户想说明的内容转成清晰的信息图，而不是套用固定业务模板。产物应当：

- **讲清关系或形态**：读者先看到结论和主结构；需要时还能看出体积、方向、包含与变换。
- **可维护**：图形定义、SVG 都是文本，可重渲染、可 diff。
- **可移植**：不写死用户路径、平台字体、业务术语或私有服务。
- **可验证**：运行语义几何 lint、SVG 产物 lint，并实际查看最终图片。
- **可组合**：能力边界由通用图形原语决定，而不是由模板、案例或领域清单决定。
- **可扩展**：画布尺寸、语言、颜色、图式、Lucide 图标与自定义 `svgGlyph` 均按任务选择。

核心 SVG 渲染无 npm 运行时依赖，只使用 Node 标准库。完整 Lucide 图标数据已经固化在 skill
内，正常出图不访问网络。PNG 是可选增强，依赖本机浏览器；找不到浏览器时仍应交付 SVG。
实际显示效果可能因本机字体而略有差异。

## 工作流

### 1. 理解内容和交付环境

先从用户材料中提取：

1. 图要表达的核心结论；
2. 读者是谁，以及图片放在哪里；
3. 必须出现的节点、关系、分组、**形态要素**（尺寸消长、通道深浅、包含关系等）和强调项；
4. 语言、尺寸、横竖版、背景和输出格式要求。

**信息量不足时，先确认信息密度（再落笔）。** 若用户在绘图前只给了题目/一句话主题、极少节点，或未说明要
概括总览还是展开细节，**不要自行假设疏密**。应调用 `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. 决定代码放置方式

默认优先在临时目录或当前任务目录出图。只有用户明确要求长期维护、提交到项目仓库时，
才初始化自包含目录：

```bash
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` 文字节。

拓扑/流程示例：

```js
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）：

```js
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. **先定视觉隐喻**：体积消长、包含嵌套、阵列汇聚、滑窗扫描、轮廓剖面等，选 1～2 个。
2. **用几何编码信息**：
   - 宽高变化 → 空间分辨率；
   - 叠层厚度/层数 → 通道或深度；
   - 圆点列 → 神经元 / 样本；
   - 扇形或柱高 → 概率或得分；
   - `path` / `svgGlyph` → 领域轮廓（叶、细胞、外壳），不要为此加行业专用 API。
   - 轮廓本身就是画面主体时，按下面「先写几何函数」那一节做，不要手写控制点。
3. **装饰与结构分离**：内部网格、示意剖面线、小核窗口等设 `lint: false`；外轮廓用 `R` +
   `track`（或 `connect` 的端点）登记，保证重叠/穿线仍可查。
4. **故意重叠要声明**：滑窗叠在输入图上时，对内层 `track(..., { allowOverlap: true })`。
5. **连线服务形态**：主干可用 `connect`；扇入、扫描、回流用 `curve` / `polyArrow`，并带
   `{ id, from, to }`；密集交叉处抽稀连线或 `allowCrossing: true`。
6. **手绘感可略加强**：`createSketch({ stroke: { roughness: 1.3, bowing: 1.15 } })`；
   正式文档可降到 `0.8` + `FONT_SANS`。
7. **标注短而靠边**：名称放在形态下方或外侧，不要把长句塞进每个小形状中心。

复杂形态图同样必须 `--lint-strict` 并通过肉眼检查；手绘抖动不是重叠的借口。

#### 形态当主体：先写几何函数，再推导内部构件

当画面主体是一个自由轮廓（叶片、细胞、器官、装置剖面、地层、地形），**不要手写贝塞尔
控制点去凑形状**，也不要把内部构件的坐标一个个写死。按三步来：

1. **用参数化函数描述轮廓**：沿主轴取归一化参数 `t`，写出该处的半宽（或半径、厚度）。
2. **轮廓由采样点平滑连成**，不手写控制点。
3. **所有内部构件都从这个函数推导**：细胞器、通道、开口、附着点、标注落点，一律用
   「该高度的宽度 × 比例」定位，而不是各写一个绝对坐标。

```js
// 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` 把采样点两两取中点做二次贝塞尔，几行即可：

```js
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. 渲染、检查、迭代

```bash
node render.mjs figures.mjs --lint-strict
node render.mjs figures.mjs --png
node render.mjs figures.mjs --only overview --lint-strict
```

`--lint` 同时执行两层辅助检查（不是完整布局证明）：

1. **语义几何 lint**：已登记节点/图标/连线的区域重叠、线穿节点或图标、**贴边擦线（净空不足）**、
   线线交叉/共线、越界；
2. **SVG 产物 lint**：非法数值、尺寸/viewBox 不一致、最终笔触和文字越界。

`lucideIcon` / `iconLabel` 默认登记；未登记的普通 `line/box/card` 不会自动进入语义层。
lint 通过不代表视觉一定正确。打开 SVG 或 PNG，按顺序确认：

1. 缩略图下主结构、阅读方向、**形态隐喻**是否一眼成立；
2. 文字与图标是否可读，有无截断、压线、图标互压或压住卡片文字；**主次字号是否分明**
   （标题明显大于节点，注解明显小于正文，且未被 `fit` 压成同一档）；
3. 顺着每条箭头能否确认起点、终点和方向，且未穿过图标；
4. 分组、留白、对齐和颜色语义是否一致；
5. 放进最终媒介后是否仍然清晰。

发现问题就调整坐标、尺寸或结构并重渲染。不要把手绘风当作重叠和歧义的理由。

### 9. 交付

默认保留图形源文件和 SVG；用户需要兼容办公软件、社交媒体或演示文稿时再导出 PNG。
交付时简要说明：

- 生成了哪些文件；
- 使用了什么尺寸和格式；
- 采用的是拓扑图式还是图形化示意（或二者组合）；
- lint 是否通过；
- PNG 若未生成，是因为缺少浏览器还是用户未请求。

不要提交临时下载、试验依赖、浏览器缓存或无关对比产物。

## 维护与开源约束

这是面向所有用户的通用开源 skill。修改时遵守：

- 新能力应扩展可表达的几何、排版、样式、连接、数据映射或导出能力，不绑定某个行业、
  公司、语言或当前案例；
- 不以模板数量、示例类型或内置组件名称定义能力范围；任何场景都可以组合原语形成新图式；
- 保持正常渲染零网络、零 npm 依赖；维护脚本可以联网，但必须固定版本并固化结果；
- 不复制维护第二份核心实现；示例应复用 `scripts/` 下的正式代码；
- 新增公共 API 时同步更新 `references/api.md`、init 种子和验证；
- 更改布局或 lint 时用简单图、复杂拓扑图与**形态示意样本**同时回归；
- 保留第三方资源的版本、来源和许可证说明（见 `lucide-icons.mjs` 头部注释）。

## 不适用场景

- 大规模数据、统计推断或交互筛选：优先使用专业图表库；简单数据图和解释性标注仍可作为
  信息图的一部分绘制；
- 照片级插画、写实封面或自由绘画：优先使用图像生成或设计工具；本 skill 做的是可复现的
  白板级图形化示意，不是写实插画；
- 用户明确要求 Mermaid、PlantUML、Figma 等其他可编辑格式：遵循用户要求；
- 一个简单文本列表比图更清楚：不要为了“有图”而画图。

