|
@@ -2,7 +2,7 @@ import Vec from '@tldraw/vec'
|
|
|
import type { Handler, WebKitGestureEvent } from '@use-gesture/core/types'
|
|
import type { Handler, WebKitGestureEvent } from '@use-gesture/core/types'
|
|
|
import { useGesture } from '@use-gesture/react'
|
|
import { useGesture } from '@use-gesture/react'
|
|
|
import * as React from 'react'
|
|
import * as React from 'react'
|
|
|
-import { TLTargetType, TLViewport } from '@tldraw/core'
|
|
|
|
|
|
|
+import { isDarwin, TLTargetType, TLViewport } from '@tldraw/core'
|
|
|
import { useRendererContext } from './useRendererContext'
|
|
import { useRendererContext } from './useRendererContext'
|
|
|
|
|
|
|
|
type PinchHandler = Handler<
|
|
type PinchHandler = Handler<
|
|
@@ -13,72 +13,98 @@ type PinchHandler = Handler<
|
|
|
export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
|
|
export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
|
|
|
const { viewport, inputs, callbacks } = useRendererContext()
|
|
const { viewport, inputs, callbacks } = useRendererContext()
|
|
|
|
|
|
|
|
|
|
+ const rOriginPoint = React.useRef<number[] | undefined>(undefined)
|
|
|
|
|
+ const rDelta = React.useRef<number[]>([0, 0])
|
|
|
|
|
+
|
|
|
const events = React.useMemo(() => {
|
|
const events = React.useMemo(() => {
|
|
|
const onWheel: Handler<'wheel', WheelEvent> = gesture => {
|
|
const onWheel: Handler<'wheel', WheelEvent> = gesture => {
|
|
|
- const { event, delta } = gesture
|
|
|
|
|
|
|
+ const { event } = gesture
|
|
|
event.preventDefault()
|
|
event.preventDefault()
|
|
|
- if (inputs.state === 'pinching') return
|
|
|
|
|
- if (Vec.isEqual(delta, [0, 0])) return
|
|
|
|
|
- callbacks.onWheel?.(
|
|
|
|
|
- {
|
|
|
|
|
- type: TLTargetType.Canvas,
|
|
|
|
|
- order: 0,
|
|
|
|
|
- delta: gesture.delta,
|
|
|
|
|
- point: inputs.currentPoint,
|
|
|
|
|
- },
|
|
|
|
|
- event
|
|
|
|
|
- )
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const [x, y, z] = normalizeWheel(event)
|
|
|
|
|
+
|
|
|
|
|
+ if (inputs.state === 'pinching') {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if ((event.altKey || event.ctrlKey || event.metaKey) && event.buttons === 0) {
|
|
|
|
|
+ const bounds = viewport.bounds
|
|
|
|
|
+ const point = inputs.currentScreenPoint ?? [bounds.width / 2, bounds.height / 2]
|
|
|
|
|
+ const delta = z / 100
|
|
|
|
|
+ const zoom = viewport.camera.zoom
|
|
|
|
|
+ viewport.onZoom(point, zoom - delta * zoom)
|
|
|
|
|
+ return
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const delta = Vec.mul(
|
|
|
|
|
+ event.shiftKey && !isDarwin()
|
|
|
|
|
+ ? // shift+scroll = pan horizontally
|
|
|
|
|
+ [y, 0]
|
|
|
|
|
+ : // scroll = pan vertically (or in any direction on a trackpad)
|
|
|
|
|
+ [x, y],
|
|
|
|
|
+ 0.8
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if (Vec.isEqual(delta, [0, 0])) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ viewport.panCamera(delta)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const onPinchStart: PinchHandler = gesture => {
|
|
|
|
|
|
|
+ const onPinchStart: PinchHandler = ({ event, delta, offset, origin }) => {
|
|
|
const elm = ref.current
|
|
const elm = ref.current
|
|
|
- const { event } = gesture
|
|
|
|
|
|
|
+ if (event instanceof WheelEvent) return
|
|
|
if (!(event.target === elm || elm?.contains(event.target as Node))) return
|
|
if (!(event.target === elm || elm?.contains(event.target as Node))) return
|
|
|
- if (!['idle', 'panning'].includes(inputs.state)) return
|
|
|
|
|
callbacks.onPinchStart?.(
|
|
callbacks.onPinchStart?.(
|
|
|
{
|
|
{
|
|
|
type: TLTargetType.Canvas,
|
|
type: TLTargetType.Canvas,
|
|
|
order: 0,
|
|
order: 0,
|
|
|
- delta: gesture.delta,
|
|
|
|
|
- offset: gesture.offset,
|
|
|
|
|
- point: Vec.sub(gesture.origin, inputs.containerOffset),
|
|
|
|
|
|
|
+ delta: [...delta, offset[0]],
|
|
|
|
|
+ offset: offset,
|
|
|
|
|
+ point: Vec.sub(origin, inputs.containerOffset),
|
|
|
},
|
|
},
|
|
|
event
|
|
event
|
|
|
)
|
|
)
|
|
|
|
|
+ rOriginPoint.current = origin
|
|
|
|
|
+ rDelta.current = [0, 0]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const onPinch: PinchHandler = gesture => {
|
|
|
|
|
|
|
+ const onPinch: PinchHandler = ({ event, offset, origin }) => {
|
|
|
const elm = ref.current
|
|
const elm = ref.current
|
|
|
- const { event } = gesture
|
|
|
|
|
|
|
+ if (event instanceof WheelEvent) return
|
|
|
if (!(event.target === elm || elm?.contains(event.target as Node))) return
|
|
if (!(event.target === elm || elm?.contains(event.target as Node))) return
|
|
|
- if (inputs.state !== 'pinching') return
|
|
|
|
|
|
|
+ const delta = Vec.sub(rOriginPoint.current, origin)
|
|
|
|
|
+ const trueDelta = Vec.sub(delta, rDelta.current)
|
|
|
callbacks.onPinch?.(
|
|
callbacks.onPinch?.(
|
|
|
{
|
|
{
|
|
|
type: TLTargetType.Canvas,
|
|
type: TLTargetType.Canvas,
|
|
|
order: 0,
|
|
order: 0,
|
|
|
- delta: gesture.delta,
|
|
|
|
|
- offset: gesture.offset,
|
|
|
|
|
- point: Vec.sub(gesture.origin, inputs.containerOffset),
|
|
|
|
|
|
|
+ delta: [...trueDelta, offset[0]],
|
|
|
|
|
+ offset: offset,
|
|
|
|
|
+ point: Vec.sub(origin, inputs.containerOffset),
|
|
|
},
|
|
},
|
|
|
event
|
|
event
|
|
|
)
|
|
)
|
|
|
|
|
+ rDelta.current = delta
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const onPinchEnd: PinchHandler = gesture => {
|
|
|
|
|
|
|
+ const onPinchEnd: PinchHandler = ({ event, delta, offset, origin }) => {
|
|
|
const elm = ref.current
|
|
const elm = ref.current
|
|
|
- const { event } = gesture
|
|
|
|
|
|
|
+ if (event instanceof WheelEvent) return
|
|
|
if (!(event.target === elm || elm?.contains(event.target as Node))) return
|
|
if (!(event.target === elm || elm?.contains(event.target as Node))) return
|
|
|
if (inputs.state !== 'pinching') return
|
|
if (inputs.state !== 'pinching') return
|
|
|
callbacks.onPinchEnd?.(
|
|
callbacks.onPinchEnd?.(
|
|
|
{
|
|
{
|
|
|
type: TLTargetType.Canvas,
|
|
type: TLTargetType.Canvas,
|
|
|
order: 0,
|
|
order: 0,
|
|
|
- delta: gesture.delta,
|
|
|
|
|
- offset: gesture.offset,
|
|
|
|
|
- point: Vec.sub(gesture.origin, inputs.containerOffset),
|
|
|
|
|
|
|
+ delta: [0, 0, offset[0]],
|
|
|
|
|
+ offset: offset,
|
|
|
|
|
+ point: Vec.sub(origin, inputs.containerOffset),
|
|
|
},
|
|
},
|
|
|
event
|
|
event
|
|
|
)
|
|
)
|
|
|
|
|
+ rDelta.current = [0, 0]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
@@ -102,3 +128,26 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
|
|
|
},
|
|
},
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// Adapted from https://stackoverflow.com/a/13650579
|
|
|
|
|
+function normalizeWheel(event: WheelEvent) {
|
|
|
|
|
+ const MAX_ZOOM_STEP = 10
|
|
|
|
|
+ const { deltaY, deltaX } = event
|
|
|
|
|
+
|
|
|
|
|
+ let deltaZ = 0
|
|
|
|
|
+
|
|
|
|
|
+ if (event.ctrlKey || event.metaKey) {
|
|
|
|
|
+ const signY = Math.sign(event.deltaY)
|
|
|
|
|
+ const absDeltaY = Math.abs(event.deltaY)
|
|
|
|
|
+
|
|
|
|
|
+ let dy = deltaY
|
|
|
|
|
+
|
|
|
|
|
+ if (absDeltaY > MAX_ZOOM_STEP) {
|
|
|
|
|
+ dy = MAX_ZOOM_STEP * signY
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ deltaZ = dy
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return [deltaX, deltaY, deltaZ]
|
|
|
|
|
+}
|