Camera options
Configure the camera's options and constraints.
import { useEffect } from 'react'
import {
BoxModel,
TLCameraOptions,
Tldraw,
Vec,
clamp,
track,
useEditor,
useLocalStorageState,
} from 'tldraw'
import 'tldraw/tldraw.css'
const CAMERA_OPTIONS: TLCameraOptions = {
isLocked: false,
wheelBehavior: 'pan',
panSpeed: 1,
zoomSpeed: 1,
zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8],
constraints: {
initialZoom: 'fit-max',
baseZoom: 'fit-max',
bounds: {
x: 0,
y: 0,
w: 1600,
h: 900,
},
behavior: { x: 'contain', y: 'contain' },
padding: { x: 100, y: 100 },
origin: { x: 0.5, y: 0.5 },
},
}
const BOUNDS_SIZES: Record<string, BoxModel> = {
a4: { x: 0, y: 0, w: 1050, h: 1485 },
landscape: { x: 0, y: 0, w: 1600, h: 900 },
portrait: { x: 0, y: 0, w: 900, h: 1600 },
square: { x: 0, y: 0, w: 900, h: 900 },
}
export default function CameraOptionsExample() {
return (
<div className="tldraw__editor">
<Tldraw
// persistenceKey="camera-options"
components={components}
>
<CameraOptionsControlPanel />
</Tldraw>
</div>
)
}
const PaddingDisplay = track(() => {
const editor = useEditor()
const cameraOptions = editor.getCameraOptions()
if (!cameraOptions.constraints) return null
const {
constraints: {
padding: { x: px, y: py },
},
} = cameraOptions
return (
<div
style={{
position: 'absolute',
top: py,
left: px,
width: `calc(100% - ${px * 2}px)`,
height: `calc(100% - ${py * 2}px)`,
border: '1px dotted var(--tl-color-text)',
pointerEvents: 'none',
}}
/>
)
})
const BoundsDisplay = track(() => {
const editor = useEditor()
const cameraOptions = editor.getCameraOptions()
if (!cameraOptions.constraints) return null
const {
constraints: {
bounds: { x, y, w, h },
},
} = cameraOptions
const d = Vec.ToAngle({ x: w, y: h }) * (180 / Math.PI)
const colB = '#00000002'
const colA = '#0000001F'
return (
<>
<div
style={{
position: 'absolute',
top: y,
left: x,
width: w,
height: h,
// grey and white stripes
border: '1px dashed var(--tl-color-text)',
backgroundImage: `
`,
backgroundSize: '200px 200px',
backgroundPosition: '0 0, 0 100px, 100px -100px, -100px 0px',
}}
>
<div
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundImage: `
linear-gradient(0deg, ${colB} 0%, ${colA} 50%, ${colB} 50%, ${colA} 100%),
linear-gradient(90deg, ${colB} 0%, ${colA} 50%, ${colB} 50%, ${colA} 100%),
linear-gradient(${d}deg, ${colB} 0%, ${colA} 50%, ${colB} 50%, ${colA} 100%),
linear-gradient(-${d}deg, ${colB} 0%, ${colA} 50%, ${colB} 50%, ${colA} 100%)`,
}}
></div>
</div>
</>
)
})
const components = {
// These components are just included for debugging / visualization!
OnTheCanvas: BoundsDisplay,
InFrontOfTheCanvas: PaddingDisplay,
}
const CameraOptionsControlPanel = track(() => {
const editor = useEditor()
const [cameraOptions, setCameraOptions] = useLocalStorageState('camera ex1', CAMERA_OPTIONS)
useEffect(() => {
if (!editor) return
editor.run(() => {
editor.setCameraOptions(cameraOptions)
editor.setCamera(editor.getCamera(), {
immediate: true,
})
})
}, [editor, cameraOptions])
const { constraints } = cameraOptions
const updateOptions = (
options: Partial<
Omit<TLCameraOptions, 'constraints'> & {
constraints: Partial<TLCameraOptions['constraints']>
}
>
) => {
const { constraints } = options
const cameraOptions = editor.getCameraOptions()
setCameraOptions({
...cameraOptions,
...options,
constraints:
constraints === undefined
? cameraOptions.constraints
: {
...(cameraOptions.constraints! ?? CAMERA_OPTIONS.constraints),
...constraints,
},
})
}
return (
<div
style={{
pointerEvents: 'all',
position: 'absolute',
top: 50,
left: 0,
padding: 4,
background: 'white',
zIndex: 100,
}}
>
<div
style={{
display: 'grid',
gridTemplateColumns: 'auto 1fr',
columnGap: 12,
rowGap: 4,
marginBottom: 12,
alignItems: 'center',
justifyContent: 'center',
}}
>
<label htmlFor="lock">Lock</label>
<select
name="lock"
value={cameraOptions.isLocked ? 'true' : 'false'}
onChange={(e) => {
const value = e.target.value
updateOptions({
...CAMERA_OPTIONS,
isLocked: value === 'true',
})
}}
>
<option value="true">true</option>
<option value="false">false</option>
</select>
<label htmlFor="wheelBehavior">Wheel behavior</label>
<select
name="wheelBehavior"
value={cameraOptions.wheelBehavior}
onChange={(e) => {
const value = e.target.value
updateOptions({
...CAMERA_OPTIONS,
wheelBehavior: value as 'zoom' | 'pan',
})
}}
>
<option>zoom</option>
<option>pan</option>
</select>
<label htmlFor="panspeed">Pan Speed</label>
<input
name="panspeed"
type="number"
step={0.1}
value={cameraOptions.panSpeed}
onChange={(e) => {
const val = clamp(Number(e.target.value), 0, 2)
updateOptions({ panSpeed: val })
}}
/>
<label htmlFor="zoomspeed">Zoom Speed</label>
<input
name="zoomspeed"
type="number"
step={0.1}
value={cameraOptions.zoomSpeed}
onChange={(e) => {
const val = clamp(Number(e.target.value), 0, 2)
updateOptions({ zoomSpeed: val })
}}
/>
<label htmlFor="zoomsteps">Zoom Steps</label>
<input
name="zoomsteps"
type="text"
defaultValue={cameraOptions.zoomSteps.join(', ')}
onChange={(e) => {
try {
const val = e.target.value.split(', ').map((v) => Number(v))
if (val.every((v) => typeof v === 'number' && Number.isFinite(v))) {
updateOptions({ zoomSteps: val })
}
} catch {
// ignore
}
}}
/>
<label htmlFor="bounds">Bounds</label>
<select
name="bounds"
value={
Object.entries(BOUNDS_SIZES).find(([_, b]) => b.w === constraints?.bounds.w)?.[0] ??
'none'
}
onChange={(e) => {
const currentConstraints = constraints ?? CAMERA_OPTIONS.constraints
const value = e.target.value
if (value === 'none') {
updateOptions({
...CAMERA_OPTIONS,
constraints: undefined,
})
return
}
updateOptions({
...CAMERA_OPTIONS,
constraints: {
...currentConstraints,
bounds: BOUNDS_SIZES[value] ?? BOUNDS_SIZES.a4,
},
})
}}
>
<option value="none">none</option>
<option value="a4">A4 Page</option>
<option value="portrait">Portait</option>
<option value="landscape">Landscape</option>
<option value="square">Square</option>
</select>
{constraints ? (
<>
<label htmlFor="initialZoom">Initial Zoom</label>
<select
name="initialZoom"
value={constraints.initialZoom}
onChange={(e) => {
updateOptions({
constraints: {
...constraints,
initialZoom: e.target.value as any,
},
})
}}
>
<option>fit-min</option>
<option>fit-max</option>
<option>fit-x</option>
<option>fit-y</option>
<option>fit-min-100</option>
<option>fit-max-100</option>
<option>fit-x-100</option>
<option>fit-y-100</option>
<option>default</option>
</select>
<label htmlFor="zoomBehavior">Base Zoom</label>
<select
name="zoomBehavior"
value={constraints.baseZoom}
onChange={(e) => {
updateOptions({
constraints: {
...constraints,
baseZoom: e.target.value as any,
},
})
}}
>
<option>fit-min</option>
<option>fit-max</option>
<option>fit-x</option>
<option>fit-y</option>
<option>fit-min-100</option>
<option>fit-max-100</option>
<option>fit-x-100</option>
<option>fit-y-100</option>
<option>default</option>
</select>
<label htmlFor="originX">Origin X</label>
<input
name="originX"
type="number"
step={0.1}
value={constraints.origin.x}
onChange={(e) => {
const val = clamp(Number(e.target.value), 0, 1)
updateOptions({
constraints: {
origin: {
...constraints.origin,
x: val,
},
},
})
}}
/>
<label htmlFor="originY">Origin Y</label>
<input
name="originY"
type="number"
step={0.1}
value={constraints.origin.y}
onChange={(e) => {
const val = clamp(Number(e.target.value), 0, 1)
updateOptions({
constraints: {
...constraints,
origin: {
...constraints.origin,
y: val,
},
},
})
}}
/>
<label htmlFor="paddingX">Padding X</label>
<input
name="paddingX"
type="number"
step={10}
value={constraints.padding.x}
onChange={(e) => {
const val = clamp(Number(e.target.value), 0)
updateOptions({
constraints: {
...constraints,
padding: {
...constraints.padding,
x: val,
},
},
})
}}
/>
<label htmlFor="paddingY">Padding Y</label>
<input
name="paddingY"
type="number"
step={10}
value={constraints.padding.y}
onChange={(e) => {
const val = clamp(Number(e.target.value), 0)
updateOptions({
constraints: {
padding: {
...constraints.padding,
y: val,
},
},
})
}}
/>
<label htmlFor="behaviorX">Behavior X</label>
<select
name="behaviorX"
value={(constraints.behavior as { x: any; y: any }).x}
onChange={(e) => {
setCameraOptions({
...cameraOptions,
constraints: {
...constraints,
behavior: {
...(constraints.behavior as { x: any; y: any }),
x: e.target.value as any,
},
},
})
}}
>
<option>free</option>
<option>contain</option>
<option>inside</option>
<option>outside</option>
<option>fixed</option>
</select>
<label htmlFor="behaviorY">Behavior Y</label>
<select
name="behaviorY"
value={(constraints.behavior as { x: any; y: any }).y}
onChange={(e) => {
setCameraOptions({
...cameraOptions,
constraints: {
...constraints,
behavior: {
...(constraints.behavior as { x: any; y: any }),
y: e.target.value as any,
},
},
})
}}
>
<option>free</option>
<option>contain</option>
<option>inside</option>
<option>outside</option>
<option>fixed</option>
</select>
</>
) : null}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<button
onClick={() => {
editor.setCamera(editor.getCamera(), { reset: true })
// eslint-disable-next-line no-console
console.log(editor.getCameraOptions())
}}
>
Reset Camera
</button>
<button
onClick={() => {
updateOptions(CAMERA_OPTIONS)
}}
>
Reset Camera Options
</button>
</div>
</div>
)
})
The Tldraw component provides a prop, cameraOptions, that can be used to set the camera's constraints, zoom behavior, and other options. This example lets you modify those options to see how they affect the camera's behavior.
Is this page helpful?
Prev
Shape optionsNext
Asset options