import type React from 'react' import type { TLBounds, TLKeyboardInfo, TLPointerInfo } from './types' import { Utils } from './utils' import { Vec } from '@tldraw/vec' const DOUBLE_CLICK_DURATION = 250 export class Inputs { pointer?: TLPointerInfo keyboard?: TLKeyboardInfo keys: Record = {} isPinching = false bounds: TLBounds = { minX: 0, maxX: 640, minY: 0, maxY: 480, width: 640, height: 480, } zoom = 1 pointerUpTime = 0 activePointer?: number pointerIsValid(e: TouchEvent | React.TouchEvent | PointerEvent | React.PointerEvent) { if ('pointerId' in e) { if (this.activePointer && this.activePointer !== e.pointerId) { return false } } if ('touches' in e) { const touch = e.changedTouches[0] if (this.activePointer && this.activePointer !== touch.identifier) { return false } } return true } touchStart(e: TouchEvent | React.TouchEvent, target: T): TLPointerInfo { const { shiftKey, ctrlKey, metaKey, altKey } = e const touch = e.changedTouches[0] this.activePointer = touch.identifier const info: TLPointerInfo = { target, pointerId: touch.identifier, origin: Inputs.getPoint(touch, this.bounds), delta: [0, 0], point: Inputs.getPoint(touch, this.bounds), pressure: Inputs.getPressure(touch), shiftKey, ctrlKey, metaKey: Utils.isDarwin() ? metaKey : ctrlKey, altKey, spaceKey: this.keys[' '], } this.pointer = info return info } touchEnd(e: TouchEvent | React.TouchEvent, target: T): TLPointerInfo { const { shiftKey, ctrlKey, metaKey, altKey } = e const touch = e.changedTouches[0] const info: TLPointerInfo = { target, pointerId: touch.identifier, origin: Inputs.getPoint(touch, this.bounds), delta: [0, 0], point: Inputs.getPoint(touch, this.bounds), pressure: Inputs.getPressure(touch), shiftKey, ctrlKey, metaKey: Utils.isDarwin() ? metaKey : ctrlKey, altKey, spaceKey: this.keys[' '], } this.pointer = info this.activePointer = undefined return info } touchMove(e: TouchEvent | React.TouchEvent, target: T): TLPointerInfo { const { shiftKey, ctrlKey, metaKey, altKey } = e const touch = e.changedTouches[0] const prev = this.pointer const point = Inputs.getPoint(touch, this.bounds) const delta = prev?.point ? Vec.sub(point, prev.point) : [0, 0] const info: TLPointerInfo = { origin: point, ...prev, target, pointerId: touch.identifier, point, delta, pressure: Inputs.getPressure(touch), shiftKey, ctrlKey, metaKey: Utils.isDarwin() ? metaKey : ctrlKey, altKey, spaceKey: this.keys[' '], } this.pointer = info return info } pointerDown(e: PointerEvent | React.PointerEvent, target: T): TLPointerInfo { const { shiftKey, ctrlKey, metaKey, altKey } = e const point = Inputs.getPoint(e, this.bounds) this.activePointer = e.pointerId const info: TLPointerInfo = { target, pointerId: e.pointerId, origin: point, point: point, delta: [0, 0], pressure: Inputs.getPressure(e), shiftKey, ctrlKey, metaKey: Utils.isDarwin() ? metaKey : ctrlKey, altKey, spaceKey: this.keys[' '], } this.pointer = info return info } pointerEnter( e: PointerEvent | React.PointerEvent, target: T ): TLPointerInfo { const { shiftKey, ctrlKey, metaKey, altKey } = e const point = Inputs.getPoint(e, this.bounds) const info: TLPointerInfo = { target, pointerId: e.pointerId, origin: point, delta: [0, 0], point: point, pressure: Inputs.getPressure(e), shiftKey, ctrlKey, metaKey: Utils.isDarwin() ? metaKey : ctrlKey, altKey, spaceKey: this.keys[' '], } this.pointer = info return info } pointerMove(e: PointerEvent | React.PointerEvent, target: T): TLPointerInfo { const { shiftKey, ctrlKey, metaKey, altKey } = e const prev = this.pointer const point = Inputs.getPoint(e, this.bounds) const delta = prev?.point ? Vec.sub(point, prev.point) : [0, 0] const info: TLPointerInfo = { origin: point, ...prev, target, pointerId: e.pointerId, point, delta, pressure: Inputs.getPressure(e), shiftKey, ctrlKey, metaKey: Utils.isDarwin() ? metaKey : ctrlKey, altKey, spaceKey: this.keys[' '], } this.pointer = info return info } pointerUp(e: PointerEvent | React.PointerEvent, target: T): TLPointerInfo { const { shiftKey, ctrlKey, metaKey, altKey } = e const prev = this.pointer const point = Inputs.getPoint(e, this.bounds) const delta = prev?.point ? Vec.sub(point, prev.point) : [0, 0] this.activePointer = undefined const info: TLPointerInfo = { origin: point, ...prev, target, pointerId: e.pointerId, point, delta, pressure: Inputs.getPressure(e), shiftKey, ctrlKey, metaKey: Utils.isDarwin() ? metaKey : ctrlKey, altKey, spaceKey: this.keys[' '], } this.pointer = info this.pointerUpTime = Date.now() return info } panStart = (e: WheelEvent): TLPointerInfo<'wheel'> => { const { shiftKey, ctrlKey, metaKey, altKey } = e const info: TLPointerInfo<'wheel'> = { target: 'wheel', pointerId: this.pointer?.pointerId || 0, origin: this.pointer?.origin || [0, 0], delta: [0, 0], pressure: 0.5, point: Inputs.getPoint(e, this.bounds), shiftKey, ctrlKey, metaKey, altKey, spaceKey: this.keys[' '], } this.pointer = info return info } pan = (delta: number[], e: WheelEvent): TLPointerInfo<'wheel'> => { if (!this.pointer || this.pointer.target !== 'wheel') { return this.panStart(e) } const { shiftKey, ctrlKey, metaKey, altKey } = e const prev = this.pointer const point = Inputs.getPoint(e, this.bounds) const info: TLPointerInfo<'wheel'> = { ...prev, target: 'wheel', delta, point, shiftKey, ctrlKey, metaKey, altKey, spaceKey: this.keys[' '], } this.pointer = info return info } isDoubleClick() { if (!this.pointer) return false const { origin, point } = this.pointer const isDoubleClick = Date.now() - this.pointerUpTime < DOUBLE_CLICK_DURATION && Vec.dist(origin, point) < 4 // Reset the active pointer, in case it got stuck if (isDoubleClick) this.activePointer = undefined return isDoubleClick } clear() { this.pointer = undefined } resetDoubleClick() { this.pointerUpTime = 0 } keydown = (e: KeyboardEvent | React.KeyboardEvent): TLKeyboardInfo => { const { shiftKey, ctrlKey, metaKey, altKey } = e this.keys[e.key] = true return { point: this.pointer?.point || [0, 0], origin: this.pointer?.origin || [0, 0], key: e.key, keys: Object.keys(this.keys), shiftKey, ctrlKey, metaKey: Utils.isDarwin() ? metaKey : ctrlKey, altKey, } } keyup = (e: KeyboardEvent | React.KeyboardEvent): TLKeyboardInfo => { const { shiftKey, ctrlKey, metaKey, altKey } = e delete this.keys[e.key] return { point: this.pointer?.point || [0, 0], origin: this.pointer?.origin || [0, 0], key: e.key, keys: Object.keys(this.keys), shiftKey, ctrlKey, metaKey: Utils.isDarwin() ? metaKey : ctrlKey, altKey, } } pinch(point: number[], origin: number[]) { const { shiftKey, ctrlKey, metaKey, altKey } = this.keys const delta = Vec.sub(origin, point) const info: TLPointerInfo<'pinch'> = { pointerId: 0, target: 'pinch', origin, delta: delta, point: Vec.sub(Vec.toFixed(point), [this.bounds.minX, this.bounds.minY]), pressure: 0.5, shiftKey, ctrlKey, metaKey: Utils.isDarwin() ? metaKey : ctrlKey, altKey, spaceKey: this.keys[' '], } this.pointer = info return info } reset() { this.pointerUpTime = 0 this.pointer = undefined this.keyboard = undefined this.activePointer = undefined this.keys = {} } static getPoint( e: PointerEvent | React.PointerEvent | Touch | React.Touch | WheelEvent, bounds: TLBounds ): number[] { return [+e.clientX.toFixed(2) - bounds.minX, +e.clientY.toFixed(2) - bounds.minY] } static getPressure(e: PointerEvent | React.PointerEvent | Touch | React.Touch | WheelEvent) { return 'pressure' in e ? +e.pressure.toFixed(2) || 0.5 : 0.5 } static commandKey(): string { return Utils.isDarwin() ? '⌘' : 'Ctrl' } } export const inputs = new Inputs()