Manna9 downloadsTurn internal links inside Canvas cards into PowerPoint-style hyperlinks: clicking a link smoothly pans and zooms the canvas to the target node, without opening a new tab.
把 Obsidian Canvas 卡片里的内部链接变成 PPT 超链接:
点击卡片内的 [[画布名.canvas#目标节点ID|显示文字]],
当前画布视口在 300~400ms 内带缓动地 平移 + 缩放,聚焦到目标节点中心。
不跳新标签页,不进入编辑模式,不破坏当前画布。
Make internal links inside Canvas cards behave like PowerPoint hyperlinks.
[[MyCanvas.canvas#nodeId|label]] inside a card → the viewport smoothly pans and zooms
(easeInOutQuad, ~350 ms, scale 1.2) to center the target node, in the same canvas/tab,
without entering edit mode.[[#nodeId]], [[MyCanvas.canvas#nodeId|label]] and [[Other.canvas#nodeId]]
(cross-canvas navigation can be disabled). Regular note links are never intercepted;
Ctrl/Cmd+click keeps the native behavior.Ctrl+Z.[[c#id|page $\alpha$]]), and hover previews of notes
are suppressed on canvas links (both toggleable).Install: copy main.js, manifest.json, styles.css into
<vault>/.obsidian/plugins/canvas-smooth-linker/ and enable it in
Settings → Community plugins. Or install from GitHub Releases via BRAT.
Verified against Obsidian 1.13.7 (Canvas internals inspected directly). Viewport math has a
68-assertion regression suite (npm test) and the bundled artifact has a 66-assertion smoke test.
把下面三个文件复制到 <你的仓库>/.obsidian/plugins/canvas-smooth-linker/:
main.js ← 已构建好的插件本体(本仓库根目录,由 main.ts 打包生成)
manifest.json
styles.css
然后在 Obsidian → 设置 → 第三方插件 里启用 Canvas Smooth Linker。
npm install
npm run dev # 监听 main.ts,改动即重新打包
npm run build # 类型检查 + 打包出生产版 main.js
npm test # 视口数学回归测试(见第 6 节)
画布节点的 ID 存在 .canvas 文件里(nodes[].id,形如 1a2b3c4d5e6f7788)。
链接写法:
| 写法 | 行为 |
|---|---|
[[画布名.canvas#1a2b3c4d5e6f7788]] |
在当前画布内平滑聚焦到该节点 |
[[画布名.canvas#1a2b3c4d5e6f7788|显示文字]] |
同上(带别名,推荐) |
[[#1a2b3c4d5e6f7788]] |
指向当前画布里的节点 |
[[别的画布.canvas#1a2b3c4d5e6f7788]] |
打开那张画布并聚焦(可关闭,见设置) |
不想手动找 ID? 在画布里选中节点,运行命令
Canvas Smooth Linker: 复制选中画布节点的内部链接,
会得到 [[路径/画布.canvas#节点ID]] 并复制到剪贴板。
把这个链接粘进卡片即可(PPT 里做「上一页 / 下一页 / 返回目录」就是这么用的)。
提示:链接必须放在能被点击的地方——画布文本卡片、或文件卡片里嵌入的笔记正文。
document 上的 捕获阶段 click 监听(外加 dblclick):
a.internal-link(必要时用 elementsFromPoint 做“点穿透”,见第 5 节);data-href → 文件部分 + # 后的节点 ID;closest(".canvas-wrapper") 反查 CanvasView(跨 popout 窗口同样注册了监听);canvas.nodes.get(nodeId) 拿节点,getBBox() → 中心点;preventDefault() + stopPropagation(),然后播放动画。Obsidian 1.13.7 的 .canvas 元素是:
.canvas {
width: 100%; height: 100%; left: 0; top: 0;
transform-origin: 0 0;
transform: translate(W/2, H/2) scale(s) translate(-x, -y);
}
其中 W/H 是画布可视区尺寸,s 是缩放倍数,x/y 是视口中心在画布坐标系里的位置。展开后:
屏幕坐标(相对 .canvas-wrapper) = (W/2, H/2) + s · (画布坐标 − 视口中心)
反解(用于从 DOM 实测当前视口,见 readViewport):
视口中心 = ( (W/2 − e) / s , (H/2 − f) / s ) // (e, f) 是 .canvas 的 CSS 矩阵平移量
让节点中心 (px, py) 落到视口中心,就是要 视口中心 := (px, py)。若必须直接写 DOM:
transform = translate(W/2 − px·s , H/2 − py·s) scale(s)
关键字段语义(已对照 1.13.7 实际代码核对):
| 字段 | 含义 |
|---|---|
canvas.x / canvas.y |
当前视口中心(画布坐标) |
canvas.tx / canvas.ty |
视口中心的动画目标(同上坐标系) |
canvas.scale |
真实缩放倍数(1 = 100%) |
canvas.zoom |
log2(scale),内部会被 clamp 到 [-4, 1],即缩放范围 1/16 ~ 2 倍 |
canvas.setViewport(x, y, zoom) |
1.13.x 的官方入口:(视口中心, log2 缩放) |
每一帧只做「按缓动曲线插值 → 落值」,不一次性跳过去:
t = clamp(elapsed / duration, 0, 1) // duration 默认 350ms
k = easeInOutQuad(t) = t < 0.5 ? 2t² : 1 − (−2t + 2)² / 2
centerX = x0 + (x1 − x0) · k // 位置:线性插值
centerY = y0 + (y1 − y0) · k
scale = 2^( log2(s0) + (log2(s1) − log2(s0)) · k ) // 缩放:对数插值(等比缩放更自然)
收敛后还会做一次 settleAndVerify 自校正:等 Obsidian 自己的 rAF 把 transform 写进 DOM 后,
从矩阵实测「屏幕上真实位置」,与目标比对(缩放偏差 > 2% / 中心偏差 > 1 画布单位才修正),
因此即使在缩放被 clamp、或版本语义略有差异时,最终也会精确落位。
动画期间一旦用户滚轮缩放或按下鼠标,动画立刻放弃,不跟用户抢视口。
不确定的字段语义都在运行时实测判定,并按优先级回退:
| 优先级 | 写入方式 | 适用 |
|---|---|---|
| ① | canvas.setViewport(cx, cy, zoom) |
1.13.x 正式入口 |
| ② | 直接写 tx/ty/x/y/zoom/tZoom/scale + markViewportChanged() |
字段直写 |
| ③ | canvas.panTo(cx, cy) + canvas.zoomBy(Δlog2) |
较老版本组合 |
| ④ | 直接写 canvasEl.style.transform |
最后兜底 |
zoom 到底是 log2(scale) 还是 scale 本身、x/y 到底是视口中心还是屏幕位移,
由 canvas.scale 与 DOM 实测矩阵交叉验证得出(detectZoomIsLog2 / readViewport);
判定不出来时按 1.13.7 的语义处理。若某版本判断错误,自校正环节会翻转语义再试一次。
zoomTo方法在 1.13.7 的 Canvas 上并不存在(只有zoomBy/zoomToBbox/zoomToFit), 所以代码没有依赖它。
styles.css 里的那行 CSS.canvas-node-content-blocker {
display: none !important;
}
Obsidian 默认在每个节点内容上盖一层透明的 .canvas-node-content-blocker,
只有节点「聚焦」后才隐藏它——这就是为什么默认必须先点一下卡片才能点到卡片里的链接。
隐藏它之后,卡片内的链接单击即可命中。
副作用与取舍:
pointerdown 监听注册在 .canvas-wrapper 的捕获阶段,
且 .canvas-wrapper 自带 user-select: none;如果你更想保留拦截层,注释掉这条规则即可——插件会用 elementsFromPoint
做「点穿透」命中测试,仍然能找到拦截层下面的链接。
| 设置 | 默认 | 说明 |
|---|---|---|
| 聚焦缩放 | 1.2 |
聚焦后的缩放倍数 |
| 动画时长 | 350ms |
限定 300~400ms |
| 接管跨画布链接 | 开 | 是否处理 [[别的画布.canvas#id]] |
| 跨画布跳转复用当前标签页 | 开 | 关掉则新标签页打开 |
| 遵循系统“减弱动态效果” | 开 | 开启系统该项时直接跳转,不播动画 |
| 找不到节点时提示 | 关 | ID 不存在时弹 Notice |
| 调试日志 | 关 | 控制台输出语义探测 / 写入方式 / 自校正过程 |
| 渲染显示文字里的 Markdown / 公式 | 开 | 见下方「链接显示文字」 |
| 悬停不弹出笔记预览 | 开 | 鼠标移到卡片链接上不再弹笔记概览 |
| 链接颜色 / 跟随主题、下划线、加粗、斜体、字号、悬停高亮 | — | 只作用于画布卡片内的链接 |
| 画布右上角显示「链接样式」按钮 | 开 | 在画布界面里直接调全局链接样式 |
右键卡片里的某一条链接,菜单里除了 Obsidian 原生的打开 / 复制链接项,还有层级菜单:
调整链接文本 ▸
颜色 ▸ 红 / 橙 / 黄 / 绿 / 蓝 / 紫 / 灰 / 默认颜色
────
自定义颜色 / HEX / 存为常用… ← 打开样式面板
大小 ▸ 字号 80% / 100% / 120% / 150% / 200%
────
自定义大小(滑块 / 百分比)… ← 打开样式面板
加粗 / 斜体 / 下划线(勾选)
清除链接文字样式
打开样式面板…
复制所在卡片链接
(下面是 Obsidian 原生项)
样式面板(由上面的「自定义…」「打开样式面板…」,或画布右上角的调色板按钮打开):
#RGB / #RRGGBB),
可「应用」或「存为常用」;「跟随主题」一键去掉自定义颜色;B 加粗 / I 斜体 / U 下划线 / 清除样式;菜单里为什么没有滑块和输入框:Obsidian 的菜单项只能放文字 / 勾选 / 图标,所以需要输入与拖动的 控件都放在这个面板里;菜单负责快速预设,面板负责精细调整。
选中的样式会写进那条链接的显示文字里,例如:
[[演示.canvas#1a2b…|<span style="color:#e05252;font-size:1.5em;font-weight:700">第 2 页</span>]]
Ctrl+Z 可撤销;画布右上角还有一个「链接样式」按钮(调色板图标):点开可以调全局的链接颜色 / 字号 / 下划线 / 加粗 / 斜体 / 悬停高亮,改完立即生效,最后一项「打开完整设置…」跳到设置页。不想要这个按钮可以在设置里关掉。
右键卡片本身 → 原生节点菜单里多了一项 复制卡片链接,等价于命令面板里的「复制选中画布节点的内部链接」。
[[链接|显示文字]] 的显示文字本来是纯文本,所以 $公式$、**粗体** 都只会原样显示。
插件会把「指向画布节点」的链接显示文字按 markdown 重新渲染一次(用 MarkdownRenderer.render
配合 finishRenderMath),于是这些都能生效:
[[演示.canvas#1a2b3c4d5e6f7788|第 2 页 $\frac{a}{b}$]]
[[演示.canvas#1a2b3c4d5e6f7788|**重点页**]]
[[演示.canvas#1a2b3c4d5e6f7788|==高亮页==]]
[[演示.canvas#1a2b3c4d5e6f7788|<span style="color:#e05252;font-weight:700">第 3 页</span>]]
关闭「渲染显示文字里的 Markdown / 公式」后,已渲染的链接会立刻恢复成纯文本。
注意:如果卡片正处于编辑状态,你看到的仍是源码,按
Esc退出编辑即恢复渲染。
.canvas-node-content a.internal-link。鼠标停在卡片链接上不再弹出笔记概览,只保留点击跳转(默认开启,可在设置里关掉)。
实现方式:Obsidian 的 Page preview 把 mouseover 挂在链接元素自身上,且不检查
defaultPrevented,所以插件在 document 的捕获阶段截断这个事件——只影响画布卡片里的链接,
普通笔记里的链接悬停预览照旧。
npm test 会在 Node 里模拟 Obsidian 的画布(4 种语义:1.13.7 标准、
缩放被版本压低的版本、旧版“屏幕位移”语义、旧版“线性 zoom”语义),断言:
easeInOutQuad 插值;当前结果:60 项断言全部通过(验证环境:Obsidian 1.13.7,插件 API 1.13.1)。
装进真实仓库后,还对本仓库产出的 main.js(与安装到仓库里的文件字节一致)做过一次
“伪 Obsidian 运行时”端到端测试:造一个 .canvas-wrapper + .canvas 矩阵 + CanvasView,
然后真的派发一次点击事件,断言:
[[画布.canvas#节点ID]] → 调用 preventDefault / stopPropagation,且不打开任何标签页;[[笔记.md#标题]]、不存在的节点 ID、Ctrl+点击 都不会被劫持;onload / onunload 正常,注册了 click / dblclick / 设置页 / 1 个命令。