首页
/ tldraw `editor.zoomToBounds()` 详解:把相机程序化地对准任意页面空间区域

tldraw `editor.zoomToBounds()` 详解:把相机程序化地对准任意页面空间区域

2026-09-07 17:15:36作者:郦嵘贵Just

本文围绕 tldraw 示例库中的 zoom-to-bounds 示例展开,讲解 editor.zoomToBounds() 这一相机 API 的完整用法:如何用页面空间的 Box{ x, y, w, h } 把视口对准指定区域,如何用 insetanimationtargetZoom 等选项控制留白与过渡效果,并结合 Editor.ts 中的源码实现与 单元测试,说明缩放比例、默认内边距与边界钳制的底层计算逻辑。读完本文,你可以在 React 应用中实现“点击按钮聚焦到某个区域/某个图形集合”的常见交互。

核心概念:什么是 zoomToBounds

editor.zoomToBounds(bounds, opts) 的作用是把相机移动到指定范围(page-space box),使该范围完整落入视口。原始说明见示例文档 README.md

  • 传入的 bounds 既可以是 Box 实例,也可以是任何 { x, y, w, h } 形状的对象;
  • 相机保持视口的宽高比,因此实际可见区域在某一轴向上通常会比你传入的 box 更大;
  • inset 在该 box 周围增加屏幕空间(像素)的留白,未指定时使用 options.zoomToFitPadding 作为默认值;
  • animation 让移动过程带缓动动画;不传则相机直接“跳”到目标位置。

这个 API 的典型使用场景包括:一键回到选中内容(zoomToSelection 的底层就是它)、展示某个模板/画布区域的“全景视角”、点击缩略图或深链接后定位到对应区域等。

完整示例:两个框 + 三种聚焦方式

仓库中的可运行示例位于 ZoomToBoundsExample.tsx,配套样式见 zoom-to-bounds.css。它的思路是:

  1. 在画布上画出两个锁定状态的矩形(紫色、蓝色),分别代表两个目标区域 zoomBox1zoomBox2
  2. 通过自定义 TopPanel 组件挂三个按钮,分别调用三种聚焦方式。

关键代码如下(摘自上述文件):

import { Box, createShapeId, Editor, TLComponents, Tldraw, TldrawUiButton, useEditor } from 'tldraw'
import 'tldraw/tldraw.css'
import './zoom-to-bounds.css'

const zoomBox1 = new Box(50, 100, 900, 720)
const zoomBox2 = new Box(1000, 500, 500, 400)

function ZoomControls() {
	const editor = useEditor()
	return (
		<div className="tlui-menu control-panel">
			{/* 直接跳转,inset 固定 72px */}
			<TldrawUiButton type="normal" onClick={() => editor.zoomToBounds(zoomBox1, { inset: 72 })}>
				Zoom to violet box
			</TldrawUiButton>
			{/* 200ms 动画 */}
			<TldrawUiButton
				type="normal"
				onClick={() => editor.zoomToBounds(zoomBox2, { inset: 72, animation: { duration: 200 } })}
			>
				Zoom to blue box
			</TldrawUiButton>
			{/* 两个框的最小外接框 + 大留白 + 动画 */}
			<TldrawUiButton
				type="normal"
				onClick={() =>
					editor.zoomToBounds(Box.Common([zoomBox1, zoomBox2]), {
						inset: 200,
						animation: { duration: 200 },
					})
				}
			>
				Zoom to both boxes
			</TldrawUiButton>
		</div>
	)
}

const components: TLComponents = {
	TopPanel: ZoomControls,
}

function handleMount(editor: Editor) {
	// 用 geo 图形在画布上“可视化”这两个 Box
	editor.createShapes([
		{
			id: createShapeId(),
			type: 'geo',
			x: zoomBox1.x,
			y: zoomBox1.y,
			isLocked: true,
			props: { w: zoomBox1.w, h: zoomBox1.h, color: 'violet' },
		},
		{
			id: createShapeId(),
			type: 'geo',
			x: zoomBox2.x,
			y: zoomBox2.y,
			isLocked: true,
			props: { w: zoomBox2.w, h: zoomBox2.h, color: 'blue' },
		},
	])
}

export default function ZoomToBoundsExample() {
	return (
		<div className="tldraw__editor">
			<Tldraw onMount={handleMount} components={components} />
		</div>
	)
}

示例中体现的三个调用模式值得记住:

调用方式 效果
editor.zoomToBounds(zoomBox1, { inset: 72 }) 立即跳到紫色框,四周留 72px 屏幕空间留白
editor.zoomToBounds(zoomBox2, { inset: 72, animation: { duration: 200 } }) 用 200ms 缓动过渡到蓝色框
editor.zoomToBounds(Box.Common([zoomBox1, zoomBox2]), { inset: 200, animation: { duration: 200 } }) 先算出两个框的最小外接框,再聚焦到该范围,留白加大到 200px

其中 Box.Common([...boxes])Box 的静态方法,用于把多个框合并为一个最小外接框——这正是“聚焦整个内容”类交互(如 zoom to all)的常用组合手法。示例文件底部也内嵌了一段注释([1] 标记处),与 README 的说明一致:相机保持宽高比,所以可见区域通常比传入的 box 在一个轴向上更大。

选项参数:insetanimationtargetZoom

zoomToBounds 的签名(见 Editor.ts)为:

zoomToBounds(bounds: BoxLike, opts?: { targetZoom?: number; inset?: number } & TLCameraMoveOptions): this

各选项说明:

  • bounds:页面空间坐标下的区域,Box{ x, y, w, h } 均可。注意坐标是页面空间(page space),不是屏幕像素。
  • inset:屏幕空间(像素)内边距,在目标 box 四周收缩有效聚焦范围。省略时,源码取 Math.min(options.zoomToFitPadding, viewportWidth * 0.28)(见下文)。
  • animation:传入 TLCameraMoveOptions 的动画配置(如 { duration: 200 });不传则相机瞬时跳转。
  • targetZoom:最终缩放倍率的上限。源码中执行 zoom = Math.min(opts.targetZoom, zoom)Editor.ts),即“聚焦到 bounds 但不超过 targetZoom 的放大程度”,常用于防止内容过小时被无限放大。
  • force(经由 TLCameraMoveOptions 透传):即使相机被锁定(isLocked)也强制执行。

官方文档注释中给出的三个典型调用(Editor.ts):

editor.zoomToBounds(myBounds)
editor.zoomToBounds(myBounds, { animation: { duration: 200 } })
editor.zoomToBounds(myBounds, { animation: { duration: 200 }, inset: 0, targetZoom: 1 })

第三个调用组合了动画、零留白和“最多放大到 100%”的约束,是“回到某个区域但不放大超过原始比例”的标准写法。

源码实现:缩放倍率是怎么算出来的

Editor.ts 的实现看,整个流程分四步:

  1. 相机锁定检查:若相机选项 isLocked 为 true 且未传 force,直接返回,不移动相机。这一点有专门测试覆盖(见下文)。
  2. 计算 inset:未显式指定时取 Math.min(this.options.zoomToFitPadding, viewportScreenBounds.width * 0.28),即默认内边距不会超过视口宽度的 28%(Editor.ts)。
  3. 求解缩放倍率
let zoom = clamp(
	Math.min(
		(viewportScreenBounds.width - inset) / bounds.w,
		(viewportScreenBounds.height - inset) / bounds.h
	),
	zoomMin * baseZoom,
	zoomMax * baseZoom
)

即分别按宽、高两个方向求出“刚好装下 box”的倍率,取较小者——这正是“保持宽高比、另一轴多出空白”的来源;再钳制到当前相机的最小/最大缩放(zoomSteps 首尾值乘 baseZoom),保证不会超出允许范围。 4. 居中并 setCamera:把 box 中心放到视口中心:

this.setCamera(
	new Vec(
		-bounds.x + (viewportScreenBounds.width - bounds.w * zoom) / 2 / zoom,
		-bounds.y + (viewportScreenBounds.height - bounds.h * zoom) / 2 / zoom,
		zoom
	),
	opts
)

setCamera 接收同一个 opts,所以 animation 配置直接驱动相机的缓动过渡。

另外两点实现细节值得注意:

  • 它是多个高级能力的底层实现:从源码结构看,zoomToFitEditor.ts)、zoomToSelectionEditor.ts)以及深链接定位(deep link bounds,Editor.ts)最终都收敛到 zoomToBounds,因此理解它就等于理解了 tldraw 大部分“聚焦”类行为。
  • 相机移动不产生撤销记录zoomToBounds 只改相机、不改文档内容,undo/redo 对它无效(有测试佐证,见下)。

默认内边距:zoomToFitPadding

inset 缺省时依赖编辑器选项 zoomToFitPadding,定义于 options.ts

/**
 * The default padding (in pixels) used when zooming to fit content in the viewport.
 * This affects methods like `zoomToFit()`, `zoomToSelection()`, and `zoomToBounds()`.
 * The actual padding used is the minimum of this value and 28% of the viewport width.
 * Defaults to 128 pixels.
 */
readonly zoomToFitPadding: number

默认值为 128 像素(options.tszoomToFitPadding: 128)。初始化 <Tldraw> 时可通过 options={{ zoomToFitPadding: 64 }} 覆盖;小视口下由于 28% 上限的存在,实际留白会随视口宽度缩小。

行为验证:单元测试给出的确定性结论

zoomToBounds.test.ts 用 1000×1000 的屏幕边界对关键行为做了断言,可以直接作为行为契约引用:

  • 居中与倍率:对 new Box(200, 300, 300, 300),最终 camera.z 等于 (1000 - padding) / 300,与源码公式完全吻合;
  • 缩放上限钳制:对 1×1 的极小 box,“does not zoom past max”——getZoomLevel() 被钳到 8(最大级别);
  • 缩放下限钳制:对 1,000,000×100,000 的巨型 box,getZoomLevel() 被钳到 0.05(最小级别);
  • 相机锁定时忽略调用setCameraOptions({ isLocked: true }) 后再调用 zoomToBounds,视口中心不变;
  • undo/redo 无感markHistoryStoppingPoint() 之后调用 zoomToBoundsundo(),相机位置不受影响;
  • 自定义 zoomToFitPadding 生效:传入 options: { zoomToFitPadding: 64 } 构造编辑器后,倍率变为 (1000 - 64) / 300

这些断言说明:zoomToBounds 的输入输出是完全确定性的,便于在应用层做可预期的区域聚焦逻辑。

小结与适用提示

editor.zoomToBounds() 是 tldraw 相机 API 中面向“区域聚焦”的基础能力:传入页面空间 box,得到一次居中、等比、带可选留白与动画的相机移动。实际开发中建议:

  • 聚焦单个区域时显式传 inset,避免默认值随视口尺寸变化带来的留白抖动;
  • 需要平滑体验时始终搭配 animation: { duration: ... }
  • 聚焦“内容集合”时先用 Box.Common(或各图形 bounds 合并)求出总包围盒再调用;
  • 不希望内容被过度放大时,用 targetZoom 兜底。

由于 zoomToFitzoomToSelection 与深链接定位都构建在它之上,掌握 zoomToBounds 的计算规则(inset 收缩 → 取宽高中较小倍率 → 钳制到 min/max → 居中 setCamera),基本就覆盖了 tldraw 中绝大多数程序化相机移动的底层原理。

登录后查看全文
热门项目推荐
相关项目推荐