
This PR implements a camera options API. - [x] Initial PR - [x] Updated unit tests - [x] Feedback / review - [x] New unit tests - [x] Update use-case examples - [x] Ship? ## Public API A user can provide camera options to the `Tldraw` component via the `cameraOptions` prop. The prop is also available on the `TldrawEditor` component and the constructor parameters of the `Editor` class. ```tsx export default function CameraOptionsExample() { return ( <div className="tldraw__editor"> <Tldraw cameraOptions={CAMERA_OPTIONS} /> </div> ) } ``` At runtime, a user can: - get the current camera options with `Editor.getCameraOptions` - update the camera options with `Editor.setCameraOptions` Setting the camera options automatically applies them to the current camera. ```ts editor.setCameraOptions({...editor.getCameraOptions(), isLocked: true }) ``` A user can get the "camera fit zoom" via `editor.getCameraFitZoom()`. # Interface The camera options themselves can look a few different ways depending on the `type` provided. ```tsx export type TLCameraOptions = { /** Whether the camera is locked. */ isLocked: boolean /** The speed of a scroll wheel / trackpad pan. Default is 1. */ panSpeed: number /** The speed of a scroll wheel / trackpad zoom. Default is 1. */ zoomSpeed: number /** The steps that a user can zoom between with zoom in / zoom out. The first and last value will determine the min and max zoom. */ zoomSteps: number[] /** Controls whether the wheel pans or zooms. * * - `zoom`: The wheel will zoom in and out. * - `pan`: The wheel will pan the camera. * - `none`: The wheel will do nothing. */ wheelBehavior: 'zoom' | 'pan' | 'none' /** The camera constraints. */ constraints?: { /** The bounds (in page space) of the constrained space */ bounds: BoxModel /** The padding inside of the viewport (in screen space) */ padding: VecLike /** The origin for placement. Used to position the bounds within the viewport when an axis is fixed or contained and zoom is below the axis fit. */ origin: VecLike /** The camera's initial zoom, used also when the camera is reset. * * - `default`: Sets the initial zoom to 100%. * - `fit-x`: The x axis will completely fill the viewport bounds. * - `fit-y`: The y axis will completely fill the viewport bounds. * - `fit-min`: The smaller axis will completely fill the viewport bounds. * - `fit-max`: The larger axis will completely fill the viewport bounds. * - `fit-x-100`: The x axis will completely fill the viewport bounds, or 100% zoom, whichever is smaller. * - `fit-y-100`: The y axis will completely fill the viewport bounds, or 100% zoom, whichever is smaller. * - `fit-min-100`: The smaller axis will completely fill the viewport bounds, or 100% zoom, whichever is smaller. * - `fit-max-100`: The larger axis will completely fill the viewport bounds, or 100% zoom, whichever is smaller. */ initialZoom: | 'fit-min' | 'fit-max' | 'fit-x' | 'fit-y' | 'fit-min-100' | 'fit-max-100' | 'fit-x-100' | 'fit-y-100' | 'default' /** The camera's base for its zoom steps. * * - `default`: Sets the initial zoom to 100%. * - `fit-x`: The x axis will completely fill the viewport bounds. * - `fit-y`: The y axis will completely fill the viewport bounds. * - `fit-min`: The smaller axis will completely fill the viewport bounds. * - `fit-max`: The larger axis will completely fill the viewport bounds. * - `fit-x-100`: The x axis will completely fill the viewport bounds, or 100% zoom, whichever is smaller. * - `fit-y-100`: The y axis will completely fill the viewport bounds, or 100% zoom, whichever is smaller. * - `fit-min-100`: The smaller axis will completely fill the viewport bounds, or 100% zoom, whichever is smaller. * - `fit-max-100`: The larger axis will completely fill the viewport bounds, or 100% zoom, whichever is smaller. */ baseZoom: | 'fit-min' | 'fit-max' | 'fit-x' | 'fit-y' | 'fit-min-100' | 'fit-max-100' | 'fit-x-100' | 'fit-y-100' | 'default' /** The behavior for the constraints for both axes or each axis individually. * * - `free`: The bounds are ignored when moving the camera. * - 'fixed': The bounds will be positioned within the viewport based on the origin * - `contain`: The 'fixed' behavior will be used when the zoom is below the zoom level at which the bounds would fill the viewport; and when above this zoom, the bounds will use the 'inside' behavior. * - `inside`: The bounds will stay completely within the viewport. * - `outside`: The bounds will stay touching the viewport. */ behavior: | 'free' | 'fixed' | 'inside' | 'outside' | 'contain' | { x: 'free' | 'fixed' | 'inside' | 'outside' | 'contain' y: 'free' | 'fixed' | 'inside' | 'outside' | 'contain' } } } ``` ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `feature` — New feature ### Test Plan These features combine in different ways, so we'll want to write some more tests to find surprises. 1. Add a step-by-step description of how to test your PR here. 2. - [ ] Unit Tests ### Release Notes - SDK: Adds camera options. --------- Co-authored-by: Mitja Bezenšek <mitja.bezensek@gmail.com>
46 lines
1.3 KiB
TypeScript
46 lines
1.3 KiB
TypeScript
import { EASINGS, StateNode, TLClickEvent } from '@tldraw/editor'
|
|
import { Dragging } from './childStates/Dragging'
|
|
import { Idle } from './childStates/Idle'
|
|
import { Pointing } from './childStates/Pointing'
|
|
|
|
/** @public */
|
|
export class HandTool extends StateNode {
|
|
static override id = 'hand'
|
|
static override initial = 'idle'
|
|
static override children = () => [Idle, Pointing, Dragging]
|
|
|
|
override onDoubleClick: TLClickEvent = (info) => {
|
|
if (info.phase === 'settle') {
|
|
const { currentScreenPoint } = this.editor.inputs
|
|
this.editor.zoomIn(currentScreenPoint, {
|
|
animation: { duration: 220, easing: EASINGS.easeOutQuint },
|
|
})
|
|
}
|
|
}
|
|
|
|
override onTripleClick: TLClickEvent = (info) => {
|
|
if (info.phase === 'settle') {
|
|
const { currentScreenPoint } = this.editor.inputs
|
|
this.editor.zoomOut(currentScreenPoint, {
|
|
animation: { duration: 320, easing: EASINGS.easeOutQuint },
|
|
})
|
|
}
|
|
}
|
|
|
|
override onQuadrupleClick: TLClickEvent = (info) => {
|
|
if (info.phase === 'settle') {
|
|
const zoomLevel = this.editor.getZoomLevel()
|
|
const {
|
|
inputs: { currentScreenPoint },
|
|
} = this.editor
|
|
|
|
if (zoomLevel === 1) {
|
|
this.editor.zoomToFit({ animation: { duration: 400, easing: EASINGS.easeOutQuint } })
|
|
} else {
|
|
this.editor.resetZoom(currentScreenPoint, {
|
|
animation: { duration: 320, easing: EASINGS.easeOutQuint },
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|