mcpField
E
excalidraw

excalidraw

github.com/djalmajr设计与 UI462 次安装5 周前更新
代码仓库

关于 excalidraw

excalidraw 是用于创建、编辑和分析 Excalidraw `.excalidraw` 场景文件的技能。小型修改可直接编写 JSON,较大的图表则可通过脚本构建、检查并渲染。

主要功能

  1. 01创建和编辑 `.excalidraw` 场景文件
  2. 02以 JSON 手动编写场景
  3. 03使用 JavaScript 工具链构建图表
  4. 04将 Mermaid 图表转换为 Excalidraw 场景
  5. 05检查几何布局并渲染 SVG 预览

适用场景

  1. 01创建架构图和流程图
  2. 02编辑或扩展现有场景
  3. 03根据场景元素概述图表内容

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/djalmajr/skills --skill excalidraw

安装后在 Claude Code 中输入/excalidraw确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

examples/
architecture.example.mjs
references/
element-reference.md
minimal-scene.excalidraw
scripts/
.gitignore
dom-shim.mjs
excalidraw-lib.mjs
lint-scene.mjs
package.json
README.md
render.mjs
setup.sh
scripts/vendor/
entry.mjs

详细介绍

excalidraw

创建和编辑 .excalidraw 文件 — 在无限画布上由 元素(矩形、文本、箭头)组成的扁平场景。根据规模,有两种创作方式:

  • 手动(JSON) — 适用于少量方框,或编辑现有场景(重新着色、重命名、重新连接、追加)。下文的元素格式和绑定/分组规则就是你需要了解的内容 — 无论采用哪种方式,都要阅读这些规则,因为程序化方式也会生成这些元素。
  • 程序化 — 适用于规模更大或需要长期维护的场景。scripts/ 中的工具链使用 Excalidraw 自身的 convertToExcalidrawElements(它会处理容易出错之处:相互绑定、容器标签、文本尺寸、z 顺序),用 exportToSvg 渲染忠实还原的 SVG 预览,并检查几何布局。参见下文的程序化创作。

.excalidraw 是 Excalidraw 自有的磁盘 JSON 格式。Mermaid 是独立的工具,但可用作实用的输入:对于标准图形(流程图、时序图、类图、ER 图),可以编写 Mermaid 并将其转换为原生 Excalidraw 场景 — 使用编辑器内置的“Mermaid to Excalidraw”,或使用 scripts/ 中的 fromMermaid()。无论采用哪种方式,之后编辑的文件都是 Excalidraw JSON,这正是本技能介绍的内容。

适用场景

  • 从头创建 .excalidraw 文件(架构图、流程图、方框和箭头图)。
  • 编辑现有场景:添加一个分区、替换其中一部分、重新着色/重命名方框、重新连接箭头。
  • 阅读场景并对其进行总结或推理(根据元素构建大纲)。

如果有可用的 Excalidraw 编辑器,且用户只是想绘图,优先使用编辑器。本技能用于直接生成/编辑文件 — 小型场景或编辑工作选择手动方式,复杂图表选择程序化方式(下一节)。

程序化创作(预览和检查循环)

对于复杂或需要维护的图表,不要手动放置几十个元素 — 使用 scripts/ 中的 JS 工具链。它封装了 Excalidraw 的官方函数,因此生成结果不会偏离格式:

  • convertToExcalidrawElements(skeleton) — Excalidraw 自有的编译器。传入高层级图形以及箭头连接的方框,它会返回有效元素,并包含相互绑定、容器标签、测量后的文本、ID 和 z 顺序。这是正确性方面最大的提升 — 下文的易错点将由它处理,而不是由你处理。
  • exportToSvg(scene) — 渲染真实效果(粗糙笔触、字体),因此预览不会像近似渲染器那样失真。

一次性设置(安装无头工具链并打包 @excalidraw/*;需要 node 和 npm,以及用于无头文本测量的 canvas/jsdom):

sh scripts/setup.sh

然后循环执行 — 构建 → 检查 → 渲染 → 查看 → 迭代:

node examples/architecture.example.mjs              # build with the Diagram builder
node scripts/lint-scene.mjs  /tmp/arch.excalidraw   # arrow crossings / label overflow / overlap
node scripts/render.mjs      /tmp/arch.excalidraw   # -> /tmp/arch.svg (faithful)
rsvg-convert -w 1200 /tmp/arch.svg -o /tmp/arch.png # rasterize, then actually LOOK

查看渲染结果是最重要的一步。 即使 JSON 有效,图表仍可能难以阅读 — 箭头穿过无关方框、标签偏离卡片。检查器能发现机械性问题;路由是否合理则要靠你自己判断。迭代修改构建器代码,而不是 JSON。

Diagram 构建器(scripts/excalidraw-lib.mjs)提供 card()、zone()、note() 和 arrow(a, b, label, {waypoints, dashed, color});arrow() 会绑定两端,并通过绝对路径点进行路由(保持路径点轴对齐,以获得整洁的正交线条)。fromMermaid() 可将 Mermaid 转换为标准类型的图表(可选依赖项)。

下文的元素格式参考仍然很重要 — 它说明了构建器会生成什么、小型场景如何手动编辑,以及如何理解现有文件。

文件封装

每个 .excalidraw 文件都是以下顶层对象:

{
  "type": "excalidraw",
  "version": 2,
  "source": "your-tool-name",
  "elements": [ /* the scene, in draw order */ ],
  "appState": { "viewBackgroundColor": "#ffffff", "gridSize": null },
  "files": {}
}
  • type / version 是固定值(当前架构为 "excalidraw" / 2)。
  • source 是由写入方标记的自由字符串(例如 "my-tool")。
  • elements 是整个场景——数组顺序就是绘制顺序(越靠后的元素越在上层)。矩形应放在其上的文本/箭头之前。
  • appState ——保持精简;大多数文件只需 viewBackgroundColor 字段。
  • files ——嵌入图像的二进制数据,以 file-id 为键。对于示意图,使用空对象 {}。

元素模型

元素是根据 type 区分的联合类型。每个元素都包含以下公共块(下面的值是安全默认值):

字段类型默认值/说明
idstring在场景中唯一。任何字符串均可("el-1"、nanoid 等)。
typestring"rectangle" | "text" | "arrow"(还有其他类型;这三种适用于大多数示意图)。
x,ynumber画布上的左上角。Y 向下递增,X 向右递增,单位为 px。
width,heightnumber边界尺寸。对于箭头,这是 points 的范围。
anglenumber旋转角度,单位为弧度。0 = 正向。
strokeColorstring轮廓/文本颜色。默认值为 #1e1e1e。
backgroundColorstring填充色。未填充的形状使用 "transparent"。
fillStylestring"solid" | "hachure" | "cross-hatch"。使用 "solid"。
strokeWidthnumber1 细,2 粗(默认值),4 特粗。
strokeStylestring"solid" | "dashed" | "dotted"。
roughnessnumber草图效果:0 建筑师(整洁),1 艺术家(默认值),2 漫画家。
opacitynumber0–100。
groupIdsstring[]分组成员身份——参见 分组。[] = 未分组。
frameIdstring | null所属框架的 ID,或 null。
roundnessobject | null圆角/曲线样式——参见下方各类型的说明。
seednumber任意整数(Excalidraw 的手绘随机数生成器种子)。任意整数均可。
versionNoncenumber任意整数;每次编辑时都会变化。
versionnumber编辑计数器;从 1 开始,每次更改时递增。
isDeletedbooleanfalse。软删除的元素会以 true 状态保留在文件中。
boundElementsarray对绑定箭头/标签的反向引用——参见 绑定。
updatednumberEpoch 毫秒时间戳(或用于确定性输出的任意单调整数)。
linkstring | null可选超链接。
lockedbooleanfalse。

index(z-order): Excalidraw 编辑器会为每个元素添加一个分数索引字符串("a0"、"a1"……),以追踪堆叠顺序。手动编写时请省略此项——编辑器会在加载时根据数组顺序分配索引。不要只为部分元素设置此项;不完整的索引会破坏 z-order。

rectangle
{ "type": "rectangle", "roundness": { "type": 3 }, /* + common fields */ }
  • roundness: { "type": 3 } → 圆角。roundness: null → 直角。
  • 带标签的方框由一个矩形加上一个独立的文本元素组成(参见 标签)。
text
{
  "type": "text",
  "text": "Login service",
  "originalText": "Login service",
  "fontSize": 16,
  "fontFamily": 2,
  "textAlign": "left",
  "verticalAlign": "top",
  "containerId": null,
  "lineHeight": 1.25,
  "baseline": 13,
  "autoResize": true,
  "roundness": null
  /* + common fields */
}
  • text 是实际渲染的内容;originalText 与其内容相同(Excalidraw 会同时保留这两个字段——除非你清楚原因,否则请保持二者一致)。
  • fontFamily:1 = 手绘风格(Virgil),2 = 常规(Helvetica,默认值),3 = 代码(Cascadia/等宽字体)。
  • width/height 必须与渲染后的文本相符,否则编辑器在重新计算前会显示异常。没有字体度量数据时,可使用以下近似值: width ≈ 最长行字符数 × fontSize × 0.58,height ≈ 行数 × fontSize × 1.25。 baseline ≈ round(fontSize × 0.8)。设置 autoResize: true 可让编辑器在加载时修正尺寸。
  • textAlign:"left" | "center" | "right"。verticalAlign:"top" | "middle" | "bottom"。
箭头
{
  "type": "arrow",
  "points": [[0, 0], [0, 80]],
  "lastCommittedPoint": null,
  "startBinding": { "elementId": "el-1", "focus": 0, "gap": 4 },
  "endBinding":   { "elementId": "el-2", "focus": 0, "gap": 4 },
  "startArrowhead": null,
  "endArrowhead": "arrow",
  "roundness": { "type": 2 }
  /* + common fields */
}
  • points 相对于箭头自身的 x/y。 第一个点是 [0, 0];箭头的 x/y 就是该点在画布绝对坐标中的位置。width/height 是这些点的边界范围。常见错误是存储绝对坐标的点——不要这样做。
  • 多段(正交/肘形)箭头只需添加更多点: [[0,0],[0,40],[120,40]] 会绘制一条先向下再向右的线。为确保路径整洁,请让线段沿轴对齐(纯水平或垂直)。
  • roundness: { "type": 2 } 会将转角变圆;null 会保留尖锐的肘形转角。
  • endArrowhead/startArrowhead:"arrow"(末端默认值)、null(无箭头),或 "triangle"、 "dot"、"bar"。
  • 使用 strokeColor 为箭头设置颜色(柔和的 #495057 作为默认颜色很清晰)。

绑定 — 让箭头跟随形状

绑定是双向且相互对应的。要将箭头绑定在两个形状之间,必须修改三个位置,否则形状移动时箭头不会跟随:

  1. arrow.startBinding = { elementId: from.id, focus: 0, gap: 4 }
  2. arrow.endBinding = { elementId: to.id, focus: 0, gap: 4 }
  3. 在每个形状的 boundElements 中添加反向引用: from.boundElements.push({ id: arrow.id, type: "arrow" }),并对 to 执行相同操作。
  • focus(-1..1)表示箭头在绑定边上的连接位置;0 = 居中。
  • gap 是箭头尖端与形状之间保留的间距(2–6 px 效果较好)。
  • 如果只设置箭头的绑定,而不设置形状的 boundElements,箭头虽然会渲染,但形状一被拖动就会脱离。务必同时设置两个方向。
  • 优先使用构建器。 diagram.arrow(a, b) 会通过官方编译器为你完成全部三项设置——只有在编辑原始 JSON 时才手动编写绑定。无论使用哪种方式,lint-scene.mjs 现在都会对任何未绑定、单向绑定或悬空的箭头报错(退出码为 1),因此在确认场景完成前请先运行它:移动时不会跟随的箭头属于错误,而不是样式小问题。

标签 — 框内文本(两种方式)

A. 容器绑定标签(Excalidraw 原生方式)。 文本会成为子元素,由编辑器自动在框内居中并换行:

  • 文本:设置 containerId = rect.id,并将其放在矩形内。
  • 矩形:boundElements 包含 { "type": "text", "id": text.id }。 编辑器会在框调整大小时重新排布标签。适合由人工继续编辑的场景。

B. 自由浮动标签(确定性方式)。 文本是独立元素,通过绝对 x/y 放置在框内,containerId: null,并且不在框的 boundElements 中引用。生成起来更简单,且完全可预测,但不会自动重新排布。适用于生成的静态图表。常见做法是制作一张“卡片”:一个圆角矩形、左上方附近的标题文本,以及下方可选的较小字号正文,三者使用相同的 groupIds。

分组

组只是每个成员的 groupIds 中共享的 id——不存在单独的组对象。要对元素分组,将相同的组 id 字符串赋给它们各自的 groupIds。嵌套组会叠加 id(最外层在最后)。若要在视觉上绘制带标签的“容器”,可在成员后方添加虚线矩形(strokeStyle: "dashed"、柔和的描边、backgroundColor: "transparent")——它就是一个普通矩形,也可以选择将其放在同一组中。

程序化路径: diagram.group(ids, { title }) 会将已放置的卡片组成一个真实的组——它会把共享的 groupId 写入每张卡片的所有部分(矩形 + 标题 + 正文),并在后方绘制可选的虚线框和标签。需要让多个区块作为一个整体移动时,请使用它;传入与 card() 相同的 ids。(groupIds 会在 convertToExcalidrawElements 后保留,因此构建器只需将其设置到骨架上。)

坐标与布局约定

  • 无限画布;可任意选择原点(例如从 0,0 附近开始)。Y 轴向下。
  • 按 x 值递增从左向右排列列,按 y 值递增从上向下堆叠行。
  • 在框之间留出间隔(例如列间留 80–90px,堆叠框之间留约 28px),为箭头留出空间,避免箭头穿过无关形状。
  • 想让箭头清晰易读,不要让框重叠。框重叠以及箭头穿过无关框,通常会导致场景杂乱。
  • 适合 Excalidraw 的易读调色板:默认描边 #1e1e1e;柔和灰色 #495057 / #868e96 / #adb5bd;强调色蓝色 #1971c2、橙色 #e8590c、紫罗兰色 #7048e8、 绿色 #2f9e44。用颜色编码含义(例如每种边类型使用一种色相),而不是单纯装饰。

阅读现有场景

解析 JSON 并遍历 elements。要生成精简的大纲(比原始 JSON 更便于模型理解):

  • 矩形 → 标签是 containerId 等于矩形 id 的文本,或位于矩形边界内的独立文本。
  • 箭头 → 将 startBinding.elementId / endBinding.elementId 解析为对应形状的标签,并渲染为 Arrow: "A" → "B"。
  • 跳过 isDeleted: true 的元素。对超大型场景进行截断(例如只取前约 200 个元素)。

大纲行示例:Rectangle "Login"、Arrow: "Login" → "Session" (label: "auth")。

更新场景中的部分内容

对现有文件实际执行的三种操作:

1. 在现有内容下方追加新区块

不要猜测坐标——先测量并移动:

  1. 计算现有元素的联合边界框(minX/minY 和 maxX = max(x+width)、maxY = max(y+height))。
  2. 计算新增元素的联合边界框。
  3. dy = existing.maxY + gap − incoming.minY(约 60px 的 gap 效果较好)。dx = 0。
  4. 将每个新增元素平移 (dx, dy)——只需将其加到各自的 x/y 上。由于箭头的 points 是相对于箭头自身的 x/y 定义的,移动 x/y 就会移动整个箭头;无需重写点坐标。绑定引用 id,因此移动后仍然有效。
  5. 拼接:existing.concat(shiftedIncoming)。
2. 替换某个区域
  1. 确定要移除的元素(按 id,或按共同的 groupIds 条目)。
  2. 从 elements 中移除这些元素(硬删除),或设置 isDeleted: true 以保留历史记录。
  3. **重新连接悬空箭头:**对于 startBinding / endBinding 指向已移除形状的箭头,要么也删除该箭头,要么将绑定的 elementId 改为指向替换形状,并更新该形状的 boundElements。同时从保留形状的 boundElements 中移除引用已删除箭头的过期条目。
  4. 插入替换元素(并按照绑定为新箭头建立绑定)。
3. 就地编辑元素

直接修改字段,然后标记变更,以便编辑器正确完成同步:

  • 增加 version(version + 1),将 versionNonce 设置为新的整数,并将 updated 设置为当前时间。
  • 对于文本变更,同时更新 text 和 originalText,并重新计算 width/height(或使用 autoResize: true)。
  • 对于移动/调整大小,更新 x/y/width/height;绑定的箭头会通过绑定自动跟随(无需编辑箭头)。
  • 要更改颜色,请修改 strokeColor / backgroundColor。

注意事项(真正会破坏场景的问题)

程序化路径(convertToExcalidrawElements)会自动处理前四项——为你设置双向绑定、相对点、文本尺寸和 z 轴顺序。这些问题主要会在手动编写时出现。

  • 单向绑定 → 拖动时箭头会脱落。务必同时设置箭头的 *Binding 和形状的 boundElements。
  • 箭头点使用绝对坐标 → 箭头会渲染在错误位置。点是相对坐标;第一个点为 [0,0]。
  • 缺少文本 width/height → 标签尺寸不正确,直到重新加载才会恢复。请估算尺寸,或设置 autoResize: true。
  • index 字段不完整 → z 轴顺序损坏。手动编写时请完全省略 index。
  • 方框重叠/箭头穿过无关形状 → 图表难以阅读。请保留间距,并使用正交线路。
  • 软删除残留项 → 即使形状的 isDeleted: true,其他位置的 boundElements 仍可能包含指向它的条目;如果想让绑定的箭头消失,请清理这些条目。
  • text/originalText 不一致 → 除非有意设置为不同值,否则请保持二者相同。

参考资料与示例

  • references/element-reference.md — 完整的逐字段表格、值枚举(fontFamily、arrowhead、roundness、appState 键)以及绑定/分组语义。
  • references/minimal-scene.excalidraw — 完整且可加载的双框箭头场景,可复制并调整。
  • scripts/ — 程序化工具链:excalidraw-lib.mjs(Diagram 构建器 + writeScene/writeSvg/fromMermaid)、render.mjs(忠实的 SVG 导出)、lint-scene.mjs(几何检查器)、setup.sh(一次性安装 + 打包)。请参阅 scripts/README.md。
  • examples/architecture.example.mjs — 使用构建器构建的可运行端到端图表。

真相来源

上述元素格式是 Excalidraw 实际可加载的最小子集,已通过 Excalidraw 0.18.1 的场景验证。编辑器在往返处理时可能会添加或保留额外字段(例如 index、自定义的逐元素数据);此处记录的字段足以编写编辑器可正常打开的文件。如果不确定此技能未涵盖的某个字段,请在 Excalidraw 编辑器中绘制一次该形状,保存并检查生成的 JSON——经过往返处理的文件才是最终依据。

评论

支持 Markdown