关于 json-canvas
json-canvas 是一项面向 Claude Code 的技能,用于创建和编辑有效的 JSON Canvas `.canvas` 文件。它说明节点、边、布局行为及验证注意事项,适合编写画布文件的用户。
主要功能
- 01创建和编辑 `.canvas` 文件
- 02使用文本、文件、链接和分组节点
- 03用带标签的边连接节点
- 04设置颜色和背景样式
- 05参考布局与验证指南
适用场景
- 01从头制作 Obsidian 画布
- 02编辑节点位置和图层
- 03构建流程图或研究画布
- 04保存前检查边和字段
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ julianobarbosa/ claude-code-skills --skill json-canvas安装后在 Claude Code 中输入/json-canvas确认已加载。
详细介绍
JSON Canvas 技能
此技能使 Claude Code 能够创建和编辑 Obsidian 及其他应用中使用的有效 JSON Canvas 文件(.canvas)。
概述
JSON Canvas 是一种用于无限画布数据的开放文件格式。Canvas 文件使用 .canvas 扩展名,并包含符合 JSON Canvas Spec 1.0 的有效 JSON。
文件结构
一个 canvas 文件包含两个顶层数组:
{
"nodes": [],
"edges": []
}
nodes(可选):节点对象数组edges(可选):连接节点的边对象数组
节点
节点是放置在画布上的对象。共有四种节点类型:
text- 带有 Markdown 的文本内容file- 对文件/附件的引用link- 外部 URLgroup- 其他节点的可视化容器
Z-Index 顺序
节点在数组中按 z-index 排序:
- 第一个节点 = 底层(显示在其他节点下方)
- 最后一个节点 = 顶层(显示在其他节点上方)
通用节点属性
所有节点共享以下属性:
| 属性 | 必需 | 类型 | 描述 |
|---|---|---|---|
id | 是 | string | 节点的唯一标识符 |
type | 是 | string | 节点类型:text、file、link 或 group |
x | 是 | integer | X 位置(像素) |
y | 是 | integer | Y 位置(像素) |
width | 是 | integer | 宽度(像素) |
height | 是 | integer | 高度(像素) |
color | 否 | canvasColor | 节点颜色(见颜色部分) |
文本节点
文本节点包含 Markdown 内容。
{
"id": "6f0ad84f44ce9c17",
"type": "text",
"x": 0,
"y": 0,
"width": 400,
"height": 200,
"text": "# Hello World\n\nThis is **Markdown** content."
}
| 属性 | 必需 | 类型 | 描述 |
|---|---|---|---|
text | 是 | string | 带有 Markdown 语法的纯文本 |
文件节点
文件节点引用文件或附件(图片、视频、PDF、笔记等)。
{
"id": "a1b2c3d4e5f67890",
"type": "file",
"x": 500,
"y": 0,
"width": 400,
"height": 300,
"file": "Attachments/diagram.png"
}
{
"id": "b2c3d4e5f6789012",
"type": "file",
"x": 500,
"y": 400,
"width": 400,
"height": 300,
"file": "Notes/Project Overview.md",
"subpath": "#Implementation"
}
| 属性 | 必需 | 类型 | 描述 |
|---|---|---|---|
file | 是 | string | 系统中文件的路径 |
subpath | 否 | string | 指向标题或块的链接(以 # 开头) |
链接节点
链接节点显示外部 URL。
{
"id": "c3d4e5f678901234",
"type": "link",
"x": 1000,
"y": 0,
"width": 400,
"height": 200,
"url": "https://obsidian.md"
}
| 属性 | 必需 | 类型 | 描述 |
|---|---|---|---|
url | 是 | string | 外部 URL |
分组节点
分组节点是用于组织其他节点的可视化容器。
{
"id": "d4e5f6789012345a",
"type": "group",
"x": -50,
"y": -50,
"width": 1000,
"height": 600,
"label": "Project Overview",
"color": "4"
}
{
"id": "e5f67890123456ab",
"type": "group",
"x": 0,
"y": 700,
"width": 800,
"height": 500,
"label": "Resources",
"background": "Attachments/background.png",
"backgroundStyle": "cover"
}
| 属性 | 必需 | 类型 | 描述 |
|---|---|---|---|
label | 否 | string | 分组的文本标签 |
background | 否 | string | 背景图片的路径 |
backgroundStyle | 否 | string | 背景渲染样式 |
背景样式
| 值 | 描述 |
|---|---|
cover | 填满节点的整个宽度和高度 |
ratio | 保持背景图片的宽高比 |
repeat | 在两个方向上以图案方式重复图片 |
边
边是连接节点的线。
{
"id": "f67890123456789a",
"fromNode": "6f0ad84f44ce9c17",
"toNode": "a1b2c3d4e5f67890"
}
{
"id": "0123456789abcdef",
"fromNode": "6f0ad84f44ce9c17",
"fromSide": "right",
"fromEnd": "none",
"toNode": "b2c3d4e5f6789012",
"toSide": "left",
"toEnd": "arrow",
"color": "1",
"label": "leads to"
}
| 属性 | 必需 | 类型 | 默认值 | 描述 |
|---|---|---|---|---|
id | 是 | string | - | 边的唯一标识符 |
fromNode | 是 | string | - | 连接起点的节点 ID |
fromSide | 否 | string | - | 边起始的一侧 |
fromEnd | 否 | string | none | 边起点处的形状 |
toNode | 是 | string | - | 连接终点的节点 ID |
toSide | 否 | string | - | 边结束的一侧 |
toEnd | 否 | string | arrow | 边终点处的形状 |
color | 否 | canvasColor | - | 线条颜色 |
label | 否 | string | - | 边的文本标签 |
侧值
| 值 | 描述 |
|---|---|
top | 节点的上边缘 |
right | 节点的右边缘 |
bottom | 节点的下边缘 |
left | 节点的左边缘 |
端点形状
| 值 | 描述 |
|---|---|
none | 无端点形状 |
arrow | 箭头端点 |
颜色
canvasColor 类型可以通过两种方式指定:
十六进制颜色
{
"color": "#FF0000"
}
预设颜色
{
"color": "1"
}
| 预设 | 颜色 |
|---|---|
"1" | 红色 |
"2" | 橙色 |
"3" | 黄色 |
"4" | 绿色 |
"5" | 青色 |
"6" | 紫色 |
注意:预设的具体颜色值未作定义,因此应用可以使用自己的品牌颜色。
完整示例
带文本和连接的简单画布
{
"nodes": [
{
"id": "8a9b0c1d2e3f4a5b",
"type": "text",
"x": 0,
"y": 0,
"width": 300,
"height": 150,
"text": "# Main Idea\n\nThis is the central concept."
},
{
"id": "1a2b3c4d5e6f7a8b",
"type": "text",
"x": 400,
"y": -100,
"width": 250,
"height": 100,
"text": "## Supporting Point A\n\nDetails here."
},
{
"id": "2b3c4d5e6f7a8b9c",
"type": "text",
"x": 400,
"y": 100,
"width": 250,
"height": 100,
"text": "## Supporting Point B\n\nMore details."
}
],
"edges": [
{
"id": "3c4d5e6f7a8b9c0d",
"fromNode": "8a9b0c1d2e3f4a5b",
"fromSide": "right",
"toNode": "1a2b3c4d5e6f7a8b",
"toSide": "left"
},
{
"id": "4d5e6f7a8b9c0d1e",
"fromNode": "8a9b0c1d2e3f4a5b",
"fromSide": "right",
"toNode": "2b3c4d5e6f7a8b9c",
"toSide": "left"
}
]
}
带分组的项目看板
{
"nodes": [
{
"id": "5e6f7a8b9c0d1e2f",
"type": "group",
"x": 0,
"y": 0,
"width": 300,
"height": 500,
"label": "To Do",
"color": "1"
},
{
"id": "6f7a8b9c0d1e2f3a",
"type": "group",
"x": 350,
"y": 0,
"width": 300,
"height": 500,
"label": "In Progress",
"color": "3"
},
{
"id": "7a8b9c0d1e2f3a4b",
"type": "group",
"x": 700,
"y": 0,
"width": 300,
"height": 500,
"label": "Done",
"color": "4"
},
{
"id": "8b9c0d1e2f3a4b5c",
"type": "text",
"x": 20,
"y": 50,
"width": 260,
"height": 80,
"text": "## Task 1\n\nImplement feature X"
},
{
"id": "9c0d1e2f3a4b5c6d",
"type": "text",
"x": 370,
"y": 50,
"width": 260,
"height": 80,
"text": "## Task 2\n\nReview PR #123",
"color": "2"
},
{
"id": "0d1e2f3a4b5c6d7e",
"type": "text",
"x": 720,
"y": 50,
"width": 260,
"height": 80,
"text": "## Task 3\n\n~~Setup CI/CD~~"
}
],
"edges": []
}
带文件和链接的研究画布
{
"nodes": [
{
"id": "1e2f3a4b5c6d7e8f",
"type": "text",
"x": 300,
"y": 200,
"width": 400,
"height": 200,
"text": "# Research Topic\n\n## Key Questions\n\n- How does X affect Y?\n- What are the implications?",
"color": "5"
},
{
"id": "2f3a4b5c6d7e8f9a",
"type": "file",
"x": 0,
"y": 0,
"width": 250,
"height": 150,
"file": "Literature/Paper A.pdf"
},
{
"id": "3a4b5c6d7e8f9a0b",
"type": "file",
"x": 0,
"y": 200,
"width": 250,
"height": 150,
"file": "Notes/Meeting Notes.md",
"subpath": "#Key Insights"
},
{
"id": "4b5c6d7e8f9a0b1c",
"type": "link",
"x": 0,
"y": 400,
"width": 250,
"height": 100,
"url": "https://example.com/research"
},
{
"id": "5c6d7e8f9a0b1c2d",
"type": "file",
"x": 750,
"y": 150,
"width": 300,
"height": 250,
"file": "Attachments/diagram.png"
}
],
"edges": [
{
"id": "6d7e8f9a0b1c2d3e",
"fromNode": "2f3a4b5c6d7e8f9a",
"fromSide": "right",
"toNode": "1e2f3a4b5c6d7e8f",
"toSide": "left",
"label": "supports"
},
{
"id": "7e8f9a0b1c2d3e4f",
"fromNode": "3a4b5c6d7e8f9a0b",
"fromSide": "right",
"toNode": "1e2f3a4b5c6d7e8f",
"toSide": "left",
"label": "informs"
},
{
"id": "8f9a0b1c2d3e4f5a",
"fromNode": "4b5c6d7e8f9a0b1c",
"fromSide": "right",
"toNode": "1e2f3a4b5c6d7e8f",
"toSide": "left",
"toEnd": "arrow",
"color": "6"
},
{
"id": "9a0b1c2d3e4f5a6b",
"fromNode": "1e2f3a4b5c6d7e8f",
"fromSide": "right",
"toNode": "5c6d7e8f9a0b1c2d",
"toSide": "left",
"label": "visualized by"
}
]
}
流程图
{
"nodes": [
{
"id": "a0b1c2d3e4f5a6b7",
"type": "text",
"x": 200,
"y": 0,
"width": 150,
"height": 60,
"text": "**Start**",
"color": "4"
},
{
"id": "b1c2d3e4f5a6b7c8",
"type": "text",
"x": 200,
"y": 100,
"width": 150,
"height": 60,
"text": "Step 1:\nGather data"
},
{
"id": "c2d3e4f5a6b7c8d9",
"type": "text",
"x": 200,
"y": 200,
"width": 150,
"height": 80,
"text": "**Decision**\n\nIs data valid?",
"color": "3"
},
{
"id": "d3e4f5a6b7c8d9e0",
"type": "text",
"x": 400,
"y": 200,
"width": 150,
"height": 60,
"text": "Process data"
},
{
"id": "e4f5a6b7c8d9e0f1",
"type": "text",
"x": 0,
"y": 200,
"width": 150,
"height": 60,
"text": "Request new data",
"color": "1"
},
{
"id": "f5a6b7c8d9e0f1a2",
"type": "text",
"x": 400,
"y": 320,
"width": 150,
"height": 60,
"text": "**End**",
"color": "4"
}
],
"edges": [
{
"id": "a6b7c8d9e0f1a2b3",
"fromNode": "a0b1c2d3e4f5a6b7",
"fromSide": "bottom",
"toNode": "b1c2d3e4f5a6b7c8",
"toSide": "top"
},
{
"id": "b7c8d9e0f1a2b3c4",
"fromNode": "b1c2d3e4f5a6b7c8",
"fromSide": "bottom",
"toNode": "c2d3e4f5a6b7c8d9",
"toSide": "top"
},
{
"id": "c8d9e0f1a2b3c4d5",
"fromNode": "c2d3e4f5a6b7c8d9",
"fromSide": "right",
"toNode": "d3e4f5a6b7c8d9e0",
"toSide": "left",
"label": "Yes",
"color": "4"
},
{
"id": "d9e0f1a2b3c4d5e6",
"fromNode": "c2d3e4f5a6b7c8d9",
"fromSide": "left",
"toNode": "e4f5a6b7c8d9e0f1",
"toSide": "right",
"label": "No",
"color": "1"
},
{
"id": "e0f1a2b3c4d5e6f7",
"fromNode": "e4f5a6b7c8d9e0f1",
"fromSide": "top",
"fromEnd": "none",
"toNode": "b1c2d3e4f5a6b7c8",
"toSide": "left",
"toEnd": "arrow"
},
{
"id": "f1a2b3c4d5e6f7a8",
"fromNode": "d3e4f5a6b7c8d9e0",
"fromSide": "bottom",
"toNode": "f5a6b7c8d9e0f1a2",
"toSide": "top"
}
]
}
ID 生成
节点和边的 ID 必须是唯一字符串。Obsidian 会生成 16 个字符的十六进制 ID:
"id": "6f0ad84f44ce9c17"
"id": "a3b2c1d0e9f8g7h6"
"id": "1234567890abcdef"
此格式为 16 个字符的小写十六进制字符串(64 位随机值)。
布局指南
定位
- 坐标可以为负数(画布会无限延伸)
x向右递增y向下递增- 位置指节点的左上角
建议尺寸
| 节点类型 | 建议宽度 | 建议高度 |
|---|---|---|
| 小型文本 | 200-300 | 80-150 |
| 中型文本 | 300-450 | 150-300 |
| 大型文本 | 400-600 | 300-500 |
| 文件预览 | 300-500 | 200-400 |
| 链接预览 | 250-400 | 100-200 |
| 分组 | 不定 | 不定 |
间距
- 在分组内留出 20-50px 的内边距
- 节点之间留出 50-100px 的间距,以提高可读性
- 将节点对齐到网格(10 或 20 的倍数),使布局更整洁
验证规则
- 所有
id值在节点和边中必须唯一 fromNode和toNode必须引用现有节点 ID- 每种节点类型都必须包含必需字段
type必须是以下值之一:text、file、link、groupbackgroundStyle必须是以下值之一:cover、ratio、repeatfromSide、toSide必须是以下值之一:top、right、bottom、leftfromEnd、toEnd必须是以下值之一:none、arrow- 颜色预设必须是
"1"到"6",或有效的十六进制颜色
参考资料
注意事项
- 节点数组的顺序就是 z-index: 读取
.canvas的编辑器会将 array[0] 渲染在底层,将 array[-1] 渲染在顶层。重新排列 JSON 来“整理”内容,会悄无声息地改变节点的遮挡关系——在打开文件前,很难从视觉差异中发现。 fromNode/toNode必须引用现有 ID,但 JSON 不会强制执行外键约束: 删除节点时,如果没有一并清理边,就会留下孤立边。Obsidian 会将它们显示为连接到空处;有些应用会静默丢弃它们,另一些则会报错。每次删除节点后都要验证边。- 坐标单位是像素,且不考虑 DPI: 在视网膜屏幕(逻辑 px)上设计的画布,在 1x 显示屏上的渲染效果会有所不同。这里没有
scale字段——应以已知的参考值预先计算尺寸,并接受不同设备上的渲染偏差。 - 16 字符十六进制 ID 是惯例,并非规范要求: 任何唯一字符串都可以使用。但 Obsidian 的“复制节点”功能会重新生成 16 字符的十六进制 ID——在同一文件中混用 UUID 和十六进制 ID,会让通过 grep 手动编辑变得很麻烦。
subpath必须带#前缀,且在某些应用中区分大小写:subpath: "Heading"会静默失败;"#Heading"可以正常工作;"#heading"是否有效则不一定——这取决于目标文件中的标题是否大小写完全一致。- 分组节点不会拥有其子节点——位置坐标才决定关系: 移动分组不会移动其中的节点。节点之所以在视觉上位于分组内,只是因为它们的 x/y 坐标恰好落在分组的边界框内。要批量移动,必须更新每个子节点的坐标。
评论
支持 Markdown