Canvas Smooth Linker
approvedby Manna
Turn 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. - This plugin has not been manually reviewed by Obsidian staff.
Canvas Smooth Linker
把 Obsidian Canvas 卡片里的内部链接变成 PPT 超链接:
点击卡片内的 [[画布名.canvas#目标节点ID|显示文字]],
当前画布视口在 300~400ms 内带缓动地 平移 + 缩放,聚焦到目标节点中心。
不跳新标签页,不进入编辑模式,不破坏当前画布。
English summary
Make internal links inside Canvas cards behave like PowerPoint hyperlinks.
- Click
[[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. - Works with
[[#nodeId]],[[MyCanvas.canvas#nodeId|label]]and[[Other.canvas#nodeId]](cross-canvas navigation can be disabled). Regular note links are never intercepted;Ctrl/Cmd+clickkeeps the native behavior. - Right-click a link → hierarchical menu Adjust link text ▸ Color / Size / Bold / Italic / Underline,
plus a floating style panel with color swatches, HEX input, saved custom colors and a font-size
slider. Styles are written into the link alias as inline HTML, so they travel with the file and
support
Ctrl+Z. - Right-click a card → Copy card link; a palette button in the canvas toolbar opens the global style panel.
- Link text supports rendered Markdown/math (
[[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.
1. 安装
方式 A:直接放进 vault(不需要 Node)
把下面三个文件复制到 <你的仓库>/.obsidian/plugins/canvas-smooth-linker/:
main.js ← 已构建好的插件本体(本仓库根目录,由 main.ts 打包生成)
manifest.json
styles.css
然后在 Obsidian → 设置 → 第三方插件 里启用 Canvas Smooth Linker。
方式 B:从源码构建
npm install
npm run dev # 监听 main.ts,改动即重新打包
npm run build # 类型检查 + 打包出生产版 main.js
npm test # 视口数学回归测试(见第 6 节)
2. 怎么写出能用的链接
画布节点的 ID 存在 .canvas 文件里(nodes[].id,形如 1a2b3c4d5e6f7788)。
链接写法:
| 写法 | 行为 |
|---|---|
[[画布名.canvas#1a2b3c4d5e6f7788]] | 在当前画布内平滑聚焦到该节点 |
[[画布名.canvas#1a2b3c4d5e6f7788|显示文字]] | 同上(带别名,推荐) |
[[#1a2b3c4d5e6f7788]] | 指向当前画布里的节点 |
[[别的画布.canvas#1a2b3c4d5e6f7788]] | 打开那张画布并聚焦(可关闭,见设置) |
不想手动找 ID? 在画布里选中节点,运行命令
Canvas Smooth Linker: 复制选中画布节点的内部链接,
会得到 [[路径/画布.canvas#节点ID]] 并复制到剪贴板。
把这个链接粘进卡片即可(PPT 里做「上一页 / 下一页 / 返回目录」就是这么用的)。
提示:链接必须放在能被点击的地方——画布文本卡片、或文件卡片里嵌入的笔记正文。
3. 它是怎么工作的
3.1 点击拦截
document 上的 捕获阶段 click 监听(外加 dblclick):
- 命中
a.internal-link(必要时用elementsFromPoint做“点穿透”,见第 5 节); - 解析
data-href→ 文件部分 +#后的节点 ID; - 按
closest(".canvas-wrapper")反查CanvasView(跨 popout 窗口同样注册了监听); canvas.nodes.get(nodeId)拿节点,getBBox()→ 中心点;preventDefault()+stopPropagation(),然后播放动画。
3.2 坐标数学(这是本插件的核心)
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 缩放) |
3.3 动画(requestAnimationFrame + easeInOutQuad)
每一帧只做「按缓动曲线插值 → 落值」,不一次性跳过去:
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、或版本语义略有差异时,最终也会精确落位。
动画期间一旦用户滚轮缩放或按下鼠标,动画立刻放弃,不跟用户抢视口。
4. 容错设计(不同 Obsidian 版本)
不确定的字段语义都在运行时实测判定,并按优先级回退:
| 优先级 | 写入方式 | 适用 |
|---|---|---|
| ① | 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), 所以代码没有依赖它。
5. 关于 styles.css 里的那行 CSS
.canvas-node-content-blocker {
display: none !important;
}
Obsidian 默认在每个节点内容上盖一层透明的 .canvas-node-content-blocker,
只有节点「聚焦」后才隐藏它——这就是为什么默认必须先点一下卡片才能点到卡片里的链接。
隐藏它之后,卡片内的链接单击即可命中。
副作用与取舍:
- 节点拖拽不受影响:画布把
pointerdown监听注册在.canvas-wrapper的捕获阶段, 且.canvas-wrapper自带user-select: none; - 代价是卡片内容本身(文本选择、内嵌视图里的控件)也变成可交互的。
如果你更想保留拦截层,注释掉这条规则即可——插件会用 elementsFromPoint
做「点穿透」命中测试,仍然能找到拦截层下面的链接。
6. 设置项
| 设置 | 默认 | 说明 |
|---|---|---|
| 聚焦缩放 | 1.2 | 聚焦后的缩放倍数 |
| 动画时长 | 350ms | 限定 300~400ms |
| 接管跨画布链接 | 开 | 是否处理 [[别的画布.canvas#id]] |
| 跨画布跳转复用当前标签页 | 开 | 关掉则新标签页打开 |
| 遵循系统“减弱动态效果” | 开 | 开启系统该项时直接跳转,不播动画 |
| 找不到节点时提示 | 关 | ID 不存在时弹 Notice |
| 调试日志 | 关 | 控制台输出语义探测 / 写入方式 / 自校正过程 |
| 渲染显示文字里的 Markdown / 公式 | 开 | 见下方「链接显示文字」 |
| 悬停不弹出笔记预览 | 开 | 鼠标移到卡片链接上不再弹笔记概览 |
| 链接颜色 / 跟随主题、下划线、加粗、斜体、字号、悬停高亮 | — | 只作用于画布卡片内的链接 |
| 画布右上角显示「链接样式」按钮 | 开 | 在画布界面里直接调全局链接样式 |
在画布界面里直接改样式(不用进设置页)
右键卡片里的某一条链接,菜单里除了 Obsidian 原生的打开 / 复制链接项,还有层级菜单:
调整链接文本 ▸
颜色 ▸ 红 / 橙 / 黄 / 绿 / 蓝 / 紫 / 灰 / 默认颜色
────
自定义颜色 / HEX / 存为常用… ← 打开样式面板
大小 ▸ 字号 80% / 100% / 120% / 150% / 200%
────
自定义大小(滑块 / 百分比)… ← 打开样式面板
加粗 / 斜体 / 下划线(勾选)
清除链接文字样式
打开样式面板…
复制所在卡片链接
(下面是 Obsidian 原生项)
样式面板(由上面的「自定义…」「打开样式面板…」,或画布右上角的调色板按钮打开):
- 颜色:色板网格(常用色 + 你保存过的自定义色)+ HEX 输入框(
#RGB/#RRGGBB), 可「应用」或「存为常用」;「跟随主题」一键去掉自定义颜色; - 大小:滑块(50%~300%)+ 百分比数字输入,拖动即时预览、松手写入;
- 样式:
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退出编辑即恢复渲染。
单个链接单独改颜色 / 样式
- 想所有画布卡片里的链接统一改:用设置里的颜色 / 下划线 / 粗细 / 斜体 / 字号;
- 想某一个链接单独改:直接把 HTML 写在显示文字里(上面第四个例子),
或者用主题 CSS 片段覆盖
.canvas-node-content a.internal-link。
悬停不弹预览
鼠标停在卡片链接上不再弹出笔记概览,只保留点击跳转(默认开启,可在设置里关掉)。
实现方式:Obsidian 的 Page preview 把 mouseover 挂在链接元素自身上,且不检查
defaultPrevented,所以插件在 document 的捕获阶段截断这个事件——只影响画布卡片里的链接,
普通笔记里的链接悬停预览照旧。
7. 测试
npm test 会在 Node 里模拟 Obsidian 的画布(4 种语义:1.13.7 标准、
缩放被版本压低的版本、旧版“屏幕位移”语义、旧版“线性 zoom”语义),断言:
- 聚焦后节点中心确实落在视口中心、缩放为 1.2;
- 用真实渲染矩阵反算的位置与目标一致;
- 视口在 300~400ms 之间抵达目标、300ms 前不会提前到达、帧数充足、位置单调不回跳、
且逐帧符合
easeInOutQuad插值; - 缩放对不齐时(例如被版本 clamp)会回滚到最佳状态,不会来回抖;
- 链接解析的 7 种写法。
当前结果:60 项断言全部通过(验证环境:Obsidian 1.13.7,插件 API 1.13.1)。
安装后的冒烟测试
装进真实仓库后,还对本仓库产出的 main.js(与安装到仓库里的文件字节一致)做过一次
“伪 Obsidian 运行时”端到端测试:造一个 .canvas-wrapper + .canvas 矩阵 + CanvasView,
然后真的派发一次点击事件,断言:
- 命中
[[画布.canvas#节点ID]]→ 调用preventDefault/stopPropagation,且不打开任何标签页; - 视口最终停在目标节点中心、缩放 1.2、多帧渲染(不是瞬移);
- 反向:
[[笔记.md#标题]]、不存在的节点 ID、Ctrl+点击都不会被劫持; onload/onunload正常,注册了 click / dblclick / 设置页 / 1 个命令。
8. 已知限制
- 只在 Canvas 内生效;普通笔记里的链接保持 Obsidian 原生行为。
- 节点 ID 不会变(重命名画布文件不影响),但删除节点后重建会换 ID, 此时旧链接会失效(插件不拦截,交回 Obsidian 原生逻辑)。
- 画布里嵌套画布(canvas embed)时会选择最内层的视口来移动。
For plugin developers
Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.