
tldraw 打造悬浮窗口效果利用 tick 事件与 setCamera 让画布钉在桌面上【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. Worlds best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw这篇指南围绕 tldraw 官方示例库中的 floaty-window悬浮窗口示例展开通过读取浏览器窗口的屏幕坐标、并在编辑器每个tick帧用editor.setCamera()做反向补偿实现画布像被钉在桌面、窗口从它上面滑过的视觉效果。读完你将掌握 tldraw 的tick事件订阅时机、相机camera坐标体系与setCamera的底层行为并能把这一轮询式视口同步思路复用到其它需要屏幕坐标驱动相机的场景。效果是什么窗口在动画布纹丝不动拖住浏览器窗口在桌面上移动你会看到 tldraw 画布里的内容并没有跟着窗口一起走——相反它像被钉在了屏幕上窗口在画布上方滑过下面露出的内容始终是同一片区域。这种反直觉的交互 2023 年底曾在社交媒体上流行过一阵子。实现的原理非常朴素窗口移动了多少相机就反向补偿多少。示例在 FloatyExample.tsx 的注释里把这个过程概括为Moving the browser window across the screen pans the camera by the same amount, so the canvas looks like its pinned to the desktop while the window slides over it.由于 README.md 记录的经验试玩时请直接拖动浏览器窗口标题栏——窗口处于最大化状态时该效果不会生效此时screenX/screenY不会随拖动变化。另外注意效果在缩放比例 zoom 为 1 时是精确的缩放后会有偏移原因见下文相机坐标一节。两个基础概念屏幕坐标与 tldraw 相机坐标动手写代码前先分清两套坐标系这是整个技巧成立的关键。window.screenX/window.screenY浏览器窗口左上角在物理屏幕坐标系中的位置单位是 CSS 像素。窗口被拖动时会实时变化最大化时恒定不变且在多显示器场景下可以是负值。tldraw 的camera{ x, y, z }三元组其中x、y表示页面坐标系原点在视口viewport中的偏移z是缩放比。它决定你把世界上的哪一块显示在屏幕上。从源码结构看tldraw 把相机建模为 store 中的一条普通记录id 为camera的instance记录。当你希望屏幕坐标不变时最直接的数学关系就是让相机x、y等于窗口屏幕坐标的负值——窗口向右移 100px相机x就减去 100px画布内容自然保持原位。这正是示例中editor.setCamera({ x: -x, y: -y })的含义页面空间坐标与屏幕坐标在 zoom1 时严格一一对应所以取负是精确的。完整示例代码不到四十行整个示例挂在 FloatyExample.tsx核心逻辑全部由一个渲染为空的 hook 组件完成import { useEffect } from react import { Tldraw, useEditor } from tldraw import tldraw/tldraw.css export default function FloatyExample() { return ( div classNametldraw__editor Tldraw persistenceKeytldraw_floaty_example FloatyWindow / /Tldraw /div ) } function FloatyWindow() { const editor useEditor() useEffect(() { let x window.screenX let y window.screenY function updateCamera() { if (window.screenX x window.screenY y) return x window.screenX y window.screenY editor.setCamera({ x: -x, y: -y }) } editor.on(tick, updateCamera) return () { editor.off(tick, updateCamera) } }, [editor]) return null }值得注意的工程细节FloatyWindow /作为Tldraw的子组件渲染因此内部可以直接调用useEditor()拿到已挂载的editor实例而组件本身return null不产生任何 DOMTldraw上使用persistenceKeytldraw_floaty_example让该示例与其它示例在本地存储中的状态互不干扰依赖数组只写[editor]编辑器实例在整个生命周期内稳定effect 只执行一次注册、一次清理。逐段拆解为什么这样写能钉住画布示例文件底部附有一段带编号的 guide这里按它的三个要点展开。[1] 子组件 useEffect在编辑器就绪后挂接逻辑任何渲染在Tldraw内部的组件都能调用useEditor()。这个 hook 组件在编辑器已经存在之后运行自己的 effect并且不渲染任何内容。这是一个非常实用的 tldraw 模式需要给编辑器加监听器、工具或副作用逻辑时不必侵入Tldraw内部包一个隐形组件即可。[2] 检测窗口位移反向设置相机每次回调开始时先把当前屏幕坐标与上次记录的坐标比较没有变化就直接return避免对相机做无意义的写入。只有检测到位移才更新缓存并调用editor.setCamera({ x: -x, y: -y })取负的原因注释里写得很清楚When they change, the camera is set to the negative of that offset so page space stays fixed relative to the screen. Camera coordinates are page-space, so this is exact at zoom 1.——相机坐标是页面空间坐标所以 zoom1 时这是精确补偿。[3] 为什么用 tick 轮询而不是监听事件浏览器没有提供窗口移动的 DOM 事件window.onresize只在尺寸变化时触发拖动过程中位置变化并不保证触发它因此最直接的兜底方案是高频轮询。示例选择订阅编辑器自身的tick事件editor.on(tick, updateCamera) return () { editor.off(tick, updateCamera) }tick在 tldraw 中默认由 TickManager.ts 驱动它内部以requestAnimationFrame建立帧循环每一帧依次emit(frame, elapsed)与emit(tick, elapsed)elapsed 是距上一帧的毫秒数。也就是说示例在每帧约 60fps都会检查一次窗口位置这个频率足够让窗口滑动、画布静止看起来完全平滑。同时在 effect 的 cleanup 里editor.off(tick, updateCamera)取消订阅——组件卸载后轮询随即停止不会造成监听器泄漏。顺带一提tick并非示例专用通道tldraw 内部大量帧驱动行为如_flushEventsForTick的事件批处理、_animateViewport的相机动画都挂在tick上事件参数类型在 emit-types.ts 中定义为tick: [number]回调会收到距上一帧的毫秒数。深入底层setCamera 到底做了什么setCamera({ x: -x, y: -y })表面只是一行调用底层链路值得展开便于你理解它为何每帧调用也无妨。入口与校验在 Editor.ts 中setCamera走的是移动视口公共路径setCameraOptions同样会以setCamera(getCamera())收尾来即时应用新约束。在写入前会经过getConstrainedCameraEditor.ts 中约 L3365 起当设置了cameraOptions缩放范围、平移约束等时请求的坐标会先被钳制到合法区间。无变化即跳过最终落地的_setCamera首先比较新旧{x, y, z}若三个分量都没变就提前返回Editor.ts 约 L3543。这正好呼应示例中先判断位移、无位移不调用的写法——即便你漏了那层判断每帧重复调用相同坐标也几乎是零成本。事务化写入且不进历史有实际位移时_setCamera通过transact包裹、以{ history: ignore }调用store.put把新相机记录写入 storeEditor.ts 约 L3547-L3554。相机移动因此不会污染撤销/重做历史也不会像普通形状操作那样进入 undo 栈。指针坐标重算相机变化后同一屏幕点对应的页面点随之改变所以_setCamera还会比较并更新当前指针的 page 坐标必要时重新派发指针事件保证拖动等交互在手势进行中也始终正确Editor.ts 约 L3558-L3568。边界条件与局限把示例拿到手跑一遍方法见下节之前先记下三个已知边界这既是原文档的提示也由源码行为佐证最大化窗口无效最大化时screenX/screenY固定不变示例的 early-return 分支会一直命中画布自然也不会补偿。文档与源码注释都明确提示了这一点。仅 zoom1 精确相机坐标是页面空间坐标位移量与屏幕像素的换算系数随z变化。示例只设x/y不动z所以在 zoom ≠ 1 时窗口移动量折算到页面空间的像素数会乘上缩放因子效果会出现漂移。需要全缩放级别精确可自行在setCamera中除以当前z。负坐标是合法的多显示器布局下副屏在左侧时screenX可能为负setCamera({ x: -x })会得到一个正数坐标属于正常情况不需要特判。在本地运行与试玩该示例位于 apps/examplestldraw 的官方示例应用。仓库内示例的编写约定可参考 writing-examples.md。在仓库根目录安装依赖后运行yarn # 启动 examples 开发服务视仓库脚本配置而定一般为 dev/examples 相关命令然后在示例列表的 UI 分类下找到名为Floaty window的条目该 README 前置元数据中title: Floaty window、priority: 30、component: ./FloatyExample.tsx打开后拖动整个浏览器窗口观察画布静止、窗口滑动。若要在调试中理解帧循环可参考 Editor.test.ts 的做法——测试中直接testEditor.emit(tick, 16)手动推一个 tick16ms来驱动依赖帧的批处理逻辑。小结一个可复用的屏幕坐标 → 相机范式floaty-window 示例虽然只有几十行却浓缩了三个可迁移的 tldraw 使用范式渲染为空的子组件 useEditor()useEffect在编辑器生命周期内注入逻辑而不侵入编辑器本体tick事件作为高频轮询时钟任何浏览器没有提供事件、但需要持续同步的状态本例是窗口位置都可以挂在这个每帧触发的通道上并记得在 cleanup 中off相机坐标 页面空间坐标凡是让画面钉住屏幕上某点的需求本质都是计算相机偏移量并调用setCamera底层事务化写入、不入 undo 历史、无位移即跳过等实现细节保证了这种每帧调用是安全高效的。理解了这条链路你就能把窗口滑过画布换成任何你想要的变体——例如把画布内容绑定到某个物理窗口区域、实现多窗口拼接画布或是在 Electron 应用中让编辑器跟随窗体的位置。【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. Worlds best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考