搜索 K
主题
在 Obsidian 里画的白板(.canvas 文件,即开放格式 JSON Canvas),能不能像一张图片一样直接嵌进已发布的文章里?@vaultkit/vitepress-plugin-obsidian-canvas 的答案是:能,而且是构建期一次成型、客户端零运行时的静态嵌入——节点、连线、箭头、标签、颜色全部保真,没有一行浏览器端 JavaScript。
本文通读这个包的全部导出与实现细节:renderCanvas 纯函数的每个选项、随包导出的完整 TypeScript 类型、CSS 变量与预设色板,以及贝塞尔连线的锚点推断这类源码里才写清楚的机制。本站主题已内置集成,你在本站文章里看到的白板嵌入就是它渲染的。
Obsidian 的 Canvas 是笔记工作流里非常高频的思考工具,但发布侧一直是个空白:官方 Publish 之外,几乎没有把 .canvas 文件渲染成网页的轻量方案。已有的一些实现要么把整个交互式画布引擎搬进浏览器(依赖重、首屏慢),要么只支持截图导出(内容失活,链接点不了)。
这个包选择了第三条路:把 JSON Canvas 解析成静态 HTML + SVG。
div,保留原始坐标系,超出容器滚动查看path,贝塞尔曲线、箭头、标签齐备pnpm add -D @vaultkit/vitepress-plugin-obsidian-canvas若使用 @vaultkit/vitepress-theme-vault 主题则开箱即带,无需单独安装——主题已经接好 ![[白板.canvas]] 的 Markdown 嵌入语法(见下文)。
renderCanvas 是一个纯函数:吃 JSON Canvas 对象,吐 HTML 字符串。在任意构建期上下文(markdown-it 插件、data loader、脚本)里调用:
import { readFileSync } from 'node:fs'
import { renderCanvas } from '@vaultkit/vitepress-plugin-obsidian-canvas'
const canvas = JSON.parse(readFileSync('思考.canvas', 'utf8'))
const html = renderCanvas(canvas, {
renderMarkdown: source => md.render(source),
resolveFile: file => ({ href: '/note-url', label: '笔记标题' }),
})样式须在主题入口引入一次(预设色板明暗两态、点阵背景都在其中):
// .vitepress/theme/index.ts
import '@vaultkit/vitepress-plugin-obsidian-canvas/styles/index.css'包的公开导出分两类。
函数:只有一个 renderCanvas(canvas, options?),返回 HTML 字符串。
类型(JSON Canvas spec 1.0 子集 + 渲染选项):
JsonCanvas——顶层结构 { nodes?, edges? }CanvasNodeBase——节点公共字段:id / type / x / y / width / height / color?(预设 '1'-'6' 或 '#RRGGBB')CanvasTextNode——type: 'text',携带 text(Markdown 源)CanvasFileNode——type: 'file',携带 file 与可选 subpath(如 '#标题',Obsidian 扩展)CanvasLinkNode——type: 'link',携带 url(外链卡)CanvasGroupNode——type: 'group',携带可选 label(虚线群组)CanvasNode——以上四种节点的联合类型CanvasSide——锚点边 'top' | 'right' | 'bottom' | 'left'CanvasEdge——连线:fromNode / toNode / fromSide? / toSide? / fromEnd? / toEnd? / color? / label?RenderCanvasOptions——渲染选项(下节逐项详解)ResolvedFileTarget——resolveFile 回调的返回结构(markdown: string) => string<br>文本节点的 Markdown 渲染回调。缺省实现只做 HTML 转义和换行处理——足够安全但没有排版。想让白板里的文本节点支持加粗、列表、代码块,把宿主的 markdown-it 实例接进来:
const html = renderCanvas(canvas, {
renderMarkdown: source => md.render(source),
})本站主题在这里接的是嵌套渲染管线(renderNested),意味着白板文本节点里连 wikilink、callout 都能正常渲染,且循环嵌入检测与嵌入深度沿用同一个嵌入栈。
(file: string, subpath?: string) => ResolvedFileTarget文件节点的解析回调,返回 ResolvedFileTarget:
href——站内链接;未发布或不存在则省略,节点渲染为灰态(带「未发布或不存在」的 title 提示)label——展示名,缺省取文件名(自动剥离路径与 .md 后缀)imageSrc——图片资源地址;返回了它,节点就直接内嵌为 <img>(object-fit: cover 铺满、懒加载)三种形态在一个回调里分流:
const html = renderCanvas(canvas, {
resolveFile: (file, subpath) => {
if (/\.(?:png|jpe?g|webp)$/i.test(file))
return { imageSrc: `/assets/${file}`, label: file }
const note = findPublishedNote(file)
if (!note)
return {} // 灰态
return { href: note.url + (subpath ? `#${slugify(subpath.slice(1))}` : ''), label: note.title }
},
})subpath 是 Obsidian 的扩展字段:白板里可以链接到笔记的某个标题(#标题),宿主负责把它 slug 化拼进锚点。
number520容器最大高度(px),超出滚动。大型白板建议调高,配合外层滚动容器移动端也能横向滑动:
renderCanvas(canvas, { maxHeight: 720 })string'Canvas 白板'容器的无障碍名。整个画布以 role="img" 输出(对读屏器而言是一张「图」,内部节点不参与朗读树),aria-label 就是这张图的替代文本。按白板内容命名:
renderCanvas(canvas, { ariaLabel: 'Canvas 白板:产品架构梳理' })细节:容器同时带 data-pagefind-ignore,白板内容不会污染 Pagefind 的全文搜索索引。
这是本包最讲究的部分,值得展开。
每条连线从起点节点的某条边(fromSide)出发,到终点节点的某条边(toSide)结束,锚点取该边中点。控制点沿锚点法向出射——从节点上边出发就先垂直向上走,从右边出发就先水平向右走——出射距离为 max(40, 两锚点距离 × 0.55)。这个 0.55 的张力系数(源码常量 EDGE_TENSION)配上 40px 的下限,是对 Obsidian 画布视觉的近似复刻:近距离连线不会挤成直线,远距离连线弧度自然舒展。
Obsidian 里随手拉的连线经常不带 fromSide / toSide。此时按两节点中心的相对方位推断:横向偏移大于纵向就走左右边(目标在右则 right → left),否则走上下边。这与 Obsidian 的缺省行为一致,保证老白板文件不做任何修补也能正确渲染。
箭头方向遵循 Obsidian 语义:缺省 fromEnd = 'none'、toEnd = 'arrow'——即只有终点有箭头;两端都要箭头或都不要,在 .canvas 文件里显式声明即可。连线标签放在三次贝塞尔曲线 t = 0.5 处的几何中点,坐标由闭式公式 (start + 3c₁ + 3c₂ + end) / 8 直接算出,无需采样;标签文字带 4px 的背景色描边(paint-order: stroke),压在线上也清晰可读。极端弯折的曲线下几何中点可能稍偏离视觉中心,这是文档明示的已知取舍。
JSON Canvas 没有显式 z-index。渲染时 group 节点垫底,其余节点按 y 坐标排序——上方的节点先画、下方的后画,视觉稳定且与 Obsidian 的观感一致。
预设色 '1'-'6'(红/橙/黄/绿/青/紫)映射为 CSS 类 oc-color-1 至 oc-color-6,色值对齐 Obsidian 画布色板,明暗两态由 CSS 变量切换。自定义 #RRGGBB 则内联生成同款「淡底 + 实边」:边框用原色,背景用 color-mix(in srgb, 原色 10%, transparent)。
![[白板.canvas]] 一行嵌入 本站主题(@vaultkit/vitepress-theme-vault)把这个包接进了 Obsidian 的嵌入语法。在文章里写:
![[产品架构.canvas]]构建期主题的 canvas-embed.ts 处理器完成全部接线:
.canvas 文件;同名冲突或不存在时输出行动导向的提示块#标题 子路径自动 slug 化为锚点;未发布的笔记灰态显示.canvas 文件挂进 env.includes,Obsidian 里改白板、浏览器里的文章页即时热更新也就是说在本站写作时你什么都不用配——白板文件放进 vault,一行 ![[]] 嵌入,效果在本站文章现场即可看到。
.vue 文件、只在构建期的 Node 环境运行,因此它是本系列中唯一不需要 vite.ssr.noExternal 配置的包。但别忘了客户端样式:styles/index.css 必须在主题入口引入一次,否则画布没有点阵背景、预设色板全部失效role="img" + aria-label 让整个白板对读屏器呈现为单一图形,避免几十个绝对定位节点被逐个朗读成噪音| 变量 | 默认值 | 说明 |
|---|---|---|
--oc-node-bg | var(--vp-c-bg-soft) | 节点底色 |
--oc-node-border | var(--vp-c-divider) | 节点边框色 |
--oc-edge | var(--vp-c-text-3) | 连线与箭头色 |
三个变量都定义在 .oc-canvas 容器上,站点级覆盖一行搞定:
.oc-canvas {
--oc-edge: var(--vp-c-brand-1);
}预设色 1-6 的类(.oc-color-*)内部也是改写这两个节点变量实现的,同一套机制。
Q:白板里的图片显示不出来?resolveFile 回调没返回 imageSrc。图片文件需要宿主解析成可访问的资源地址——本站主题走的是父页相对路径 + Vite 资产管线;裸用时返回 public 目录下的绝对路径也可以。
Q:文本节点里的 Markdown 只有纯文本? 缺省 renderMarkdown 只做转义。把宿主的 markdown-it render 接进来即可获得完整排版。
Q:连线标签为什么偏离了曲线中心? 标签位置是三次贝塞尔 t = 0.5 的几何中点,极端弯折(比如两节点几乎重叠又强制指定对侧锚点)时会偏。调整白板布局或去掉显式锚点让推断接管即可。