input-observer.ts 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923
  1. /**
  2. * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. * @author David Sehnal <david.sehnal@gmail.com>
  6. */
  7. import { Subject, Observable } from 'rxjs';
  8. import { Viewport } from '../../mol-canvas3d/camera/util';
  9. import { Vec2, EPSILON } from '../../mol-math/linear-algebra';
  10. import { BitFlags, noop } from '../../mol-util';
  11. export function getButtons(event: MouseEvent | Touch) {
  12. if (typeof event === 'object') {
  13. if ('buttons' in event) {
  14. return event.buttons;
  15. } else if ('which' in event) {
  16. const b = (event as any).which; // 'any' to support older browsers
  17. if (b === 2) {
  18. return 4;
  19. } else if (b === 3) {
  20. return 2;
  21. } else if (b > 0) {
  22. return 1 << (b - 1);
  23. }
  24. }
  25. }
  26. return 0;
  27. }
  28. export function getButton(event: MouseEvent | Touch) {
  29. if (typeof event === 'object') {
  30. if ('button' in event) {
  31. const b = event.button;
  32. if (b === 1) {
  33. return 4;
  34. } else if (b === 2) {
  35. return 2;
  36. } else if (b >= 0) {
  37. return 1 << b;
  38. }
  39. }
  40. }
  41. return 0;
  42. }
  43. export function getModifiers(event: MouseEvent | Touch): ModifiersKeys {
  44. return {
  45. alt: 'altKey' in event ? event.altKey : false,
  46. shift: 'shiftKey' in event ? event.shiftKey : false,
  47. control: 'ctrlKey' in event ? event.ctrlKey : false,
  48. meta: 'metaKey' in event ? event.metaKey : false
  49. };
  50. }
  51. export const DefaultInputObserverProps = {
  52. noScroll: true,
  53. noMiddleClickScroll: true,
  54. noContextMenu: true,
  55. noPinchZoom: true,
  56. noTextSelect: true,
  57. preventGestures: false,
  58. mask: (x: number, y: number) => true,
  59. pixelScale: 1
  60. };
  61. export type InputObserverProps = Partial<typeof DefaultInputObserverProps>
  62. export type ModifiersKeys = {
  63. shift: boolean,
  64. alt: boolean,
  65. control: boolean,
  66. meta: boolean
  67. }
  68. export namespace ModifiersKeys {
  69. export const None = create();
  70. export function areEqual(a: ModifiersKeys, b: ModifiersKeys) {
  71. return a.shift === b.shift && a.alt === b.alt && a.control === b.control && a.meta === b.meta;
  72. }
  73. export function size(a?: ModifiersKeys) {
  74. if (!a) return 0;
  75. let ret = 0;
  76. if (!!a.shift) ret++;
  77. if (!!a.alt) ret++;
  78. if (!!a.control) ret++;
  79. if (!!a.meta) ret++;
  80. return ret;
  81. }
  82. export function create(modifierKeys: Partial<ModifiersKeys> = {}): ModifiersKeys {
  83. return {
  84. shift: !!modifierKeys.shift,
  85. alt: !!modifierKeys.alt,
  86. control: !!modifierKeys.control,
  87. meta: !!modifierKeys.meta
  88. };
  89. }
  90. }
  91. export type ButtonsType = BitFlags<ButtonsType.Flag>
  92. export namespace ButtonsType {
  93. export const has: (btn: ButtonsType, f: Flag) => boolean = BitFlags.has;
  94. export const create: (fs: Flag) => ButtonsType = BitFlags.create;
  95. export enum Flag {
  96. /** No button or un-initialized */
  97. None = 0x0,
  98. /** Primary button (usually left) */
  99. Primary = 0x1,
  100. /** Secondary button (usually right) */
  101. Secondary = 0x2,
  102. /** Auxilary button (usually middle or mouse wheel button) */
  103. Auxilary = 0x4,
  104. /** 4th button (typically the "Browser Back" button) */
  105. Forth = 0x8,
  106. /** 5th button (typically the "Browser Forward" button) */
  107. Five = 0x10,
  108. }
  109. }
  110. export type KeyCode = string
  111. type BaseInput = {
  112. buttons: ButtonsType
  113. button: ButtonsType.Flag
  114. modifiers: ModifiersKeys
  115. }
  116. export type DragInput = {
  117. x: number,
  118. y: number,
  119. dx: number,
  120. dy: number,
  121. pageX: number,
  122. pageY: number,
  123. isStart: boolean
  124. } & BaseInput
  125. export type WheelInput = {
  126. x: number,
  127. y: number,
  128. pageX: number,
  129. pageY: number,
  130. dx: number,
  131. dy: number,
  132. dz: number,
  133. spinX: number,
  134. spinY: number
  135. } & BaseInput
  136. export type ClickInput = {
  137. x: number,
  138. y: number,
  139. pageX: number,
  140. pageY: number,
  141. } & BaseInput
  142. export type MoveInput = {
  143. x: number,
  144. y: number,
  145. pageX: number,
  146. pageY: number,
  147. movementX?: number,
  148. movementY?: number,
  149. inside: boolean,
  150. // Move is subscribed to window element
  151. // This indicates that the event originated from the element the InputObserver was created on
  152. onElement: boolean
  153. } & BaseInput
  154. export type PinchInput = {
  155. delta: number,
  156. fraction: number,
  157. fractionDelta: number,
  158. distance: number,
  159. isStart: boolean
  160. } & BaseInput
  161. export type GestureInput = {
  162. scale: number,
  163. rotation: number,
  164. deltaScale: number,
  165. deltaRotation: number
  166. isStart?: boolean,
  167. isEnd?: boolean
  168. }
  169. export type KeyInput = {
  170. key: string,
  171. code: string,
  172. modifiers: ModifiersKeys
  173. /** for overwriting browser shortcuts like `ctrl+s` as needed */
  174. preventDefault: () => void
  175. }
  176. export const EmptyKeyInput: KeyInput = {
  177. key: '',
  178. code: '',
  179. modifiers: ModifiersKeys.None,
  180. preventDefault: noop,
  181. };
  182. export type ResizeInput = {
  183. }
  184. enum DraggingState {
  185. Stopped = 0,
  186. Started = 1,
  187. Moving = 2
  188. }
  189. type PointerEvent = {
  190. clientX: number
  191. clientY: number
  192. pageX: number
  193. pageY: number
  194. movementX?: number
  195. movementY?: number
  196. target: EventTarget | null
  197. preventDefault?: () => void
  198. }
  199. type GestureEvent = {
  200. scale: number,
  201. rotation: number,
  202. } & MouseEvent
  203. interface InputObserver {
  204. noScroll: boolean
  205. noContextMenu: boolean
  206. readonly width: number
  207. readonly height: number
  208. readonly pixelRatio: number
  209. readonly pointerLock: boolean
  210. readonly drag: Observable<DragInput>,
  211. // Equivalent to mouseUp and touchEnd
  212. readonly interactionEnd: Observable<undefined>,
  213. readonly wheel: Observable<WheelInput>,
  214. readonly pinch: Observable<PinchInput>,
  215. readonly gesture: Observable<GestureInput>,
  216. readonly click: Observable<ClickInput>,
  217. readonly move: Observable<MoveInput>,
  218. readonly leave: Observable<undefined>,
  219. readonly enter: Observable<undefined>,
  220. readonly resize: Observable<ResizeInput>,
  221. readonly modifiers: Observable<ModifiersKeys>
  222. readonly key: Observable<KeyInput>
  223. readonly keyUp: Observable<KeyInput>
  224. readonly keyDown: Observable<KeyInput>
  225. readonly lock: Observable<boolean>
  226. requestPointerLock: (viewport: Viewport) => void
  227. exitPointerLock: () => void
  228. dispose: () => void
  229. }
  230. function createEvents() {
  231. return {
  232. drag: new Subject<DragInput>(),
  233. interactionEnd: new Subject<undefined>(),
  234. click: new Subject<ClickInput>(),
  235. move: new Subject<MoveInput>(),
  236. wheel: new Subject<WheelInput>(),
  237. pinch: new Subject<PinchInput>(),
  238. gesture: new Subject<GestureInput>(),
  239. resize: new Subject<ResizeInput>(),
  240. leave: new Subject<undefined>(),
  241. enter: new Subject<undefined>(),
  242. modifiers: new Subject<ModifiersKeys>(),
  243. key: new Subject<KeyInput>(),
  244. keyUp: new Subject<KeyInput>(),
  245. keyDown: new Subject<KeyInput>(),
  246. lock: new Subject<boolean>(),
  247. };
  248. }
  249. const AllowedNonPrintableKeys = ['Backspace', 'Delete'];
  250. namespace InputObserver {
  251. export function create(props: InputObserverProps = {}): InputObserver {
  252. const { noScroll, noContextMenu } = { ...DefaultInputObserverProps, ...props };
  253. return {
  254. noScroll,
  255. noContextMenu,
  256. pointerLock: false,
  257. width: 0,
  258. height: 0,
  259. pixelRatio: 1,
  260. ...createEvents(),
  261. requestPointerLock: noop,
  262. exitPointerLock: noop,
  263. dispose: noop
  264. };
  265. }
  266. export function fromElement(element: Element, props: InputObserverProps = {}): InputObserver {
  267. let { noScroll, noMiddleClickScroll, noContextMenu, noPinchZoom, noTextSelect, mask, pixelScale, preventGestures } = { ...DefaultInputObserverProps, ...props };
  268. let width = element.clientWidth * pixelRatio();
  269. let height = element.clientHeight * pixelRatio();
  270. let isLocked = false;
  271. let lockedViewport = Viewport();
  272. let lastTouchDistance = 0, lastTouchFraction = 0;
  273. const pointerDown = Vec2();
  274. const pointerStart = Vec2();
  275. const pointerEnd = Vec2();
  276. const pointerDelta = Vec2();
  277. const rectSize = Vec2();
  278. const modifierKeys: ModifiersKeys = {
  279. shift: false,
  280. alt: false,
  281. control: false,
  282. meta: false
  283. };
  284. function pixelRatio() {
  285. return window.devicePixelRatio * pixelScale;
  286. }
  287. function getModifierKeys(): ModifiersKeys {
  288. return { ...modifierKeys };
  289. }
  290. function getKeyOnElement(event: Event): boolean {
  291. return event.target === document.body || event.target === element;
  292. }
  293. let dragging: DraggingState = DraggingState.Stopped;
  294. let disposed = false;
  295. let buttons = ButtonsType.create(ButtonsType.Flag.None);
  296. let button = ButtonsType.Flag.None;
  297. let isInside = false;
  298. let hasMoved = false;
  299. const events = createEvents();
  300. const { drag, interactionEnd, wheel, pinch, gesture, click, move, leave, enter, resize, modifiers, key, keyUp, keyDown, lock } = events;
  301. attach();
  302. function attach() {
  303. element.addEventListener('contextmenu', onContextMenu as any, false);
  304. element.addEventListener('wheel', onMouseWheel as any, false);
  305. element.addEventListener('mousedown', onMouseDown as any, false);
  306. // for dragging to work outside canvas bounds,
  307. // mouse move/up events have to be added to a parent, i.e. window
  308. window.addEventListener('mousemove', onMouseMove as any, false);
  309. window.addEventListener('mouseup', onMouseUp as any, false);
  310. element.addEventListener('touchstart', onTouchStart as any, false);
  311. element.addEventListener('touchmove', onTouchMove as any, false);
  312. element.addEventListener('touchend', onTouchEnd as any, false);
  313. element.addEventListener('gesturechange', onGestureChange as any, false);
  314. element.addEventListener('gesturestart', onGestureStart as any, false);
  315. element.addEventListener('gestureend', onGestureEnd as any, false);
  316. // reset buttons and modifier keys state when browser window looses focus
  317. window.addEventListener('blur', handleBlur);
  318. window.addEventListener('keyup', handleKeyUp as EventListener, false);
  319. window.addEventListener('keydown', handleKeyDown as EventListener, false);
  320. window.addEventListener('keypress', handleKeyPress as EventListener, false);
  321. document.addEventListener('pointerlockchange', onPointerLockChange, false);
  322. document.addEventListener('pointerlockerror', onPointerLockError, false);
  323. window.addEventListener('resize', onResize, false);
  324. }
  325. function dispose() {
  326. if (disposed) return;
  327. disposed = true;
  328. element.removeEventListener('contextmenu', onContextMenu as any, false);
  329. element.removeEventListener('wheel', onMouseWheel as any, false);
  330. element.removeEventListener('mousedown', onMouseDown as any, false);
  331. window.removeEventListener('mousemove', onMouseMove as any, false);
  332. window.removeEventListener('mouseup', onMouseUp as any, false);
  333. element.removeEventListener('touchstart', onTouchStart as any, false);
  334. element.removeEventListener('touchmove', onTouchMove as any, false);
  335. element.removeEventListener('touchend', onTouchEnd as any, false);
  336. element.removeEventListener('gesturechange', onGestureChange as any, false);
  337. element.removeEventListener('gesturestart', onGestureStart as any, false);
  338. element.removeEventListener('gestureend', onGestureEnd as any, false);
  339. window.removeEventListener('blur', handleBlur);
  340. window.removeEventListener('keyup', handleKeyUp as EventListener, false);
  341. window.removeEventListener('keydown', handleKeyDown as EventListener, false);
  342. window.removeEventListener('keypress', handleKeyPress as EventListener, false);
  343. document.removeEventListener('pointerlockchange', onPointerLockChange, false);
  344. document.removeEventListener('pointerlockerror', onPointerLockError, false);
  345. window.removeEventListener('resize', onResize, false);
  346. cross.remove();
  347. }
  348. function onPointerLockChange() {
  349. if (element.ownerDocument.pointerLockElement === element) {
  350. isLocked = true;
  351. } else {
  352. isLocked = false;
  353. }
  354. toggleCross(isLocked);
  355. lock.next(isLocked);
  356. }
  357. function onPointerLockError() {
  358. console.error('Unable to use Pointer Lock API');
  359. isLocked = false;
  360. toggleCross(isLocked);
  361. lock.next(isLocked);
  362. }
  363. function onContextMenu(event: MouseEvent) {
  364. if (!mask(event.clientX, event.clientY)) return;
  365. if (noContextMenu) {
  366. event.preventDefault();
  367. }
  368. }
  369. function updateModifierKeys(event: MouseEvent | WheelEvent | TouchEvent) {
  370. modifierKeys.alt = event.altKey;
  371. modifierKeys.shift = event.shiftKey;
  372. modifierKeys.control = event.ctrlKey;
  373. modifierKeys.meta = event.metaKey;
  374. }
  375. function handleBlur() {
  376. if (buttons || modifierKeys.shift || modifierKeys.alt || modifierKeys.meta || modifierKeys.control) {
  377. buttons = 0 as ButtonsType;
  378. modifierKeys.shift = modifierKeys.alt = modifierKeys.control = modifierKeys.meta = false;
  379. }
  380. }
  381. function handleKeyDown(event: KeyboardEvent) {
  382. let changed = false;
  383. if (!modifierKeys.alt && event.altKey) { changed = true; modifierKeys.alt = true; }
  384. if (!modifierKeys.shift && event.shiftKey) { changed = true; modifierKeys.shift = true; }
  385. if (!modifierKeys.control && event.ctrlKey) { changed = true; modifierKeys.control = true; }
  386. if (!modifierKeys.meta && event.metaKey) { changed = true; modifierKeys.meta = true; }
  387. if (changed && isInside) modifiers.next(getModifierKeys());
  388. if (getKeyOnElement(event) && isInside) {
  389. keyDown.next({
  390. key: event.key,
  391. code: event.code,
  392. modifiers: getModifierKeys(),
  393. preventDefault: () => event.preventDefault(),
  394. });
  395. }
  396. }
  397. function handleKeyUp(event: KeyboardEvent) {
  398. let changed = false;
  399. if (modifierKeys.alt && !event.altKey) { changed = true; modifierKeys.alt = false; }
  400. if (modifierKeys.shift && !event.shiftKey) { changed = true; modifierKeys.shift = false; }
  401. if (modifierKeys.control && !event.ctrlKey) { changed = true; modifierKeys.control = false; }
  402. if (modifierKeys.meta && !event.metaKey) { changed = true; modifierKeys.meta = false; }
  403. if (changed && isInside) modifiers.next(getModifierKeys());
  404. if (AllowedNonPrintableKeys.includes(event.key)) handleKeyPress(event);
  405. if (getKeyOnElement(event) && isInside) {
  406. keyUp.next({
  407. key: event.key,
  408. code: event.code,
  409. modifiers: getModifierKeys(),
  410. preventDefault: () => event.preventDefault(),
  411. });
  412. }
  413. }
  414. function handleKeyPress(event: KeyboardEvent) {
  415. if (!getKeyOnElement(event) || !isInside) return;
  416. key.next({
  417. key: event.key,
  418. code: event.code,
  419. modifiers: getModifierKeys(),
  420. preventDefault: () => event.preventDefault(),
  421. });
  422. }
  423. function getCenterTouch(ev: TouchEvent): PointerEvent {
  424. const t0 = ev.touches[0];
  425. const t1 = ev.touches[1];
  426. return {
  427. clientX: (t0.clientX + t1.clientX) / 2,
  428. clientY: (t0.clientY + t1.clientY) / 2,
  429. pageX: (t0.pageX + t1.pageX) / 2,
  430. pageY: (t0.pageY + t1.pageY) / 2,
  431. target: ev.target
  432. };
  433. }
  434. function getTouchDistance(ev: TouchEvent) {
  435. const dx = ev.touches[0].pageX - ev.touches[1].pageX;
  436. const dy = ev.touches[0].pageY - ev.touches[1].pageY;
  437. return Math.sqrt(dx * dx + dy * dy);
  438. }
  439. function onTouchStart(ev: TouchEvent) {
  440. ev.preventDefault();
  441. if (ev.touches.length === 1) {
  442. buttons = button = ButtonsType.Flag.Primary;
  443. onPointerDown(ev.touches[0]);
  444. } else if (ev.touches.length === 2) {
  445. buttons = ButtonsType.Flag.Secondary & ButtonsType.Flag.Auxilary;
  446. button = ButtonsType.Flag.Secondary;
  447. onPointerDown(getCenterTouch(ev));
  448. const touchDistance = getTouchDistance(ev);
  449. lastTouchDistance = touchDistance;
  450. pinch.next({
  451. distance: touchDistance,
  452. fraction: 1,
  453. fractionDelta: 0,
  454. delta: 0,
  455. isStart: true,
  456. buttons,
  457. button,
  458. modifiers: getModifierKeys()
  459. });
  460. } else if (ev.touches.length === 3) {
  461. buttons = button = ButtonsType.Flag.Forth;
  462. onPointerDown(getCenterTouch(ev));
  463. }
  464. }
  465. function onTouchEnd(ev: TouchEvent) {
  466. endDrag();
  467. }
  468. function onTouchMove(ev: TouchEvent) {
  469. button = ButtonsType.Flag.None;
  470. if (noPinchZoom) {
  471. ev.preventDefault();
  472. ev.stopPropagation();
  473. if ((ev as any).originalEvent) {
  474. (ev as any).originalEvent.preventDefault();
  475. (ev as any).originalEvent.stopPropagation();
  476. }
  477. }
  478. if (ev.touches.length === 1) {
  479. buttons = ButtonsType.Flag.Primary;
  480. onPointerMove(ev.touches[0]);
  481. } else if (ev.touches.length === 2) {
  482. const touchDistance = getTouchDistance(ev);
  483. const touchDelta = lastTouchDistance - touchDistance;
  484. if (Math.abs(touchDelta) < 4) {
  485. buttons = ButtonsType.Flag.Secondary;
  486. onPointerMove(getCenterTouch(ev));
  487. } else {
  488. buttons = ButtonsType.Flag.Auxilary;
  489. updateModifierKeys(ev);
  490. const fraction = lastTouchDistance / touchDistance;
  491. pinch.next({
  492. delta: touchDelta,
  493. fraction,
  494. fractionDelta: lastTouchFraction - fraction,
  495. distance: touchDistance,
  496. isStart: false,
  497. buttons,
  498. button,
  499. modifiers: getModifierKeys()
  500. });
  501. lastTouchFraction = fraction;
  502. }
  503. lastTouchDistance = touchDistance;
  504. } else if (ev.touches.length === 3) {
  505. buttons = ButtonsType.Flag.Forth;
  506. onPointerMove(getCenterTouch(ev));
  507. }
  508. }
  509. function onMouseDown(ev: MouseEvent) {
  510. updateModifierKeys(ev);
  511. buttons = getButtons(ev);
  512. button = getButton(ev);
  513. if (noMiddleClickScroll && buttons === ButtonsType.Flag.Auxilary) {
  514. ev.preventDefault;
  515. }
  516. onPointerDown(ev);
  517. }
  518. function onMouseMove(ev: MouseEvent) {
  519. updateModifierKeys(ev);
  520. buttons = getButtons(ev);
  521. button = ButtonsType.Flag.None;
  522. onPointerMove(ev);
  523. }
  524. function onMouseUp(ev: MouseEvent) {
  525. updateModifierKeys(ev);
  526. buttons = getButtons(ev);
  527. button = getButton(ev);
  528. onPointerUp(ev);
  529. endDrag();
  530. }
  531. function endDrag() {
  532. interactionEnd.next(void 0);
  533. }
  534. function onPointerDown(ev: PointerEvent) {
  535. if (!mask(ev.clientX, ev.clientY)) return;
  536. eventOffset(pointerStart, ev);
  537. Vec2.copy(pointerDown, pointerStart);
  538. if (insideBounds(pointerStart)) {
  539. dragging = DraggingState.Started;
  540. }
  541. }
  542. function onPointerUp(ev: PointerEvent) {
  543. dragging = DraggingState.Stopped;
  544. if (!mask(ev.clientX, ev.clientY)) return;
  545. eventOffset(pointerEnd, ev);
  546. if (!hasMoved && Vec2.distance(pointerEnd, pointerDown) < 4) {
  547. const { pageX, pageY } = getPagePosition(ev);
  548. const [x, y] = pointerEnd;
  549. click.next({ x, y, pageX, pageY, buttons, button, modifiers: getModifierKeys() });
  550. }
  551. hasMoved = false;
  552. }
  553. function onPointerMove(ev: PointerEvent) {
  554. eventOffset(pointerEnd, ev);
  555. const { pageX, pageY } = getPagePosition(ev);
  556. const [x, y] = pointerEnd;
  557. const { movementX, movementY } = ev;
  558. const inside = insideBounds(pointerEnd) && mask(ev.clientX, ev.clientY);
  559. if (isInside && !inside) {
  560. leave.next(void 0);
  561. } else if (!isInside && inside) {
  562. enter.next(void 0);
  563. }
  564. isInside = inside;
  565. move.next({ x, y, pageX, pageY, movementX, movementY, buttons, button, modifiers: getModifierKeys(), inside, onElement: ev.target === element });
  566. if (dragging === DraggingState.Stopped) return;
  567. if (noTextSelect) {
  568. ev.preventDefault?.();
  569. }
  570. Vec2.div(pointerDelta, Vec2.sub(pointerDelta, pointerEnd, pointerStart), getClientSize(rectSize));
  571. if (Vec2.magnitude(pointerDelta) < EPSILON) return;
  572. const isStart = dragging === DraggingState.Started;
  573. if (isStart && !mask(ev.clientX, ev.clientY)) return;
  574. if (Vec2.distance(pointerEnd, pointerDown) >= 4) {
  575. hasMoved = true;
  576. }
  577. const [dx, dy] = pointerDelta;
  578. drag.next({ x, y, dx, dy, pageX, pageY, buttons, button, modifiers: getModifierKeys(), isStart });
  579. Vec2.copy(pointerStart, pointerEnd);
  580. dragging = DraggingState.Moving;
  581. }
  582. function onMouseWheel(ev: WheelEvent) {
  583. if (!mask(ev.clientX, ev.clientY)) return;
  584. eventOffset(pointerEnd, ev);
  585. const { pageX, pageY } = getPagePosition(ev);
  586. const [x, y] = pointerEnd;
  587. if (noScroll) {
  588. ev.preventDefault();
  589. }
  590. const normalized = normalizeWheel(ev);
  591. buttons = button = ButtonsType.Flag.Auxilary;
  592. if (normalized.dx || normalized.dy || normalized.dz) {
  593. wheel.next({ x, y, pageX, pageY, ...normalized, buttons, button, modifiers: getModifierKeys() });
  594. }
  595. }
  596. function tryPreventGesture(ev: GestureEvent) {
  597. // console.log(ev, preventGestures);
  598. if (!preventGestures) return;
  599. ev.preventDefault();
  600. ev.stopImmediatePropagation?.();
  601. ev.stopPropagation?.();
  602. }
  603. let prevGestureScale = 0, prevGestureRotation = 0;
  604. function onGestureStart(ev: GestureEvent) {
  605. tryPreventGesture(ev);
  606. prevGestureScale = ev.scale;
  607. prevGestureRotation = ev.rotation;
  608. gesture.next({ scale: ev.scale, rotation: ev.rotation, deltaRotation: 0, deltaScale: 0, isStart: true });
  609. }
  610. function gestureDelta(ev: GestureEvent, isEnd?: boolean) {
  611. gesture.next({
  612. scale: ev.scale,
  613. rotation: ev.rotation,
  614. deltaRotation: prevGestureRotation - ev.rotation,
  615. deltaScale: prevGestureScale - ev.scale,
  616. isEnd
  617. });
  618. prevGestureRotation = ev.rotation;
  619. prevGestureScale = ev.scale;
  620. }
  621. function onGestureChange(ev: GestureEvent) {
  622. tryPreventGesture(ev);
  623. gestureDelta(ev);
  624. }
  625. function onGestureEnd(ev: GestureEvent) {
  626. tryPreventGesture(ev);
  627. gestureDelta(ev, true);
  628. }
  629. function onResize(ev: Event) {
  630. resize.next({});
  631. }
  632. function insideBounds(pos: Vec2) {
  633. if (element instanceof Window || element instanceof Document || element === document.body) {
  634. return true;
  635. } else {
  636. const rect = element.getBoundingClientRect();
  637. return pos[0] >= 0 && pos[1] >= 0 && pos[0] < rect.width && pos[1] < rect.height;
  638. }
  639. }
  640. function getClientSize(out: Vec2) {
  641. out[0] = element.clientWidth;
  642. out[1] = element.clientHeight;
  643. return out;
  644. }
  645. function eventOffset(out: Vec2, ev: PointerEvent) {
  646. width = element.clientWidth * pixelRatio();
  647. height = element.clientHeight * pixelRatio();
  648. if (isLocked) {
  649. const pr = pixelRatio();
  650. out[0] = (lockedViewport.x + lockedViewport.width / 2) / pr;
  651. out[1] = (height - (lockedViewport.y + lockedViewport.height / 2)) / pr;
  652. } else {
  653. const rect = element.getBoundingClientRect();
  654. out[0] = (ev.clientX || 0) - rect.left;
  655. out[1] = (ev.clientY || 0) - rect.top;
  656. }
  657. return out;
  658. }
  659. function getPagePosition(ev: PointerEvent) {
  660. if (isLocked) {
  661. return {
  662. pageX: Math.round(window.innerWidth / 2) + lockedViewport.x,
  663. pageY: Math.round(window.innerHeight / 2) + lockedViewport.y
  664. };
  665. } else {
  666. return {
  667. pageX: ev.pageX,
  668. pageY: ev.pageY
  669. };
  670. }
  671. }
  672. const cross = addCross();
  673. const crossWidth = 30;
  674. function addCross() {
  675. const cross = document.createElement('div');
  676. const b = '30%';
  677. const t = '10%';
  678. const c = `#000 ${b}, #0000 0 calc(100% - ${b}), #000 0`;
  679. const vline = `linear-gradient(0deg, ${c}) 50%/${t} 100% no-repeat`;
  680. const hline = `linear-gradient(90deg, ${c}) 50%/100% ${t} no-repeat`;
  681. const cdot = 'radial-gradient(circle at 50%, #000 5%, #0000 5%)';
  682. Object.assign(cross.style, {
  683. width: `${crossWidth}px`,
  684. aspectRatio: 1,
  685. background: `${vline}, ${hline}, ${cdot}`,
  686. display: 'none',
  687. zIndex: 1000,
  688. position: 'absolute',
  689. mixBlendMode: 'difference',
  690. filter: 'invert(1)',
  691. });
  692. element.parentElement?.appendChild(cross);
  693. return cross;
  694. }
  695. function toggleCross(value: boolean) {
  696. cross.style.display = value ? 'block' : 'none';
  697. if (value) {
  698. const pr = pixelRatio();
  699. const offsetX = (lockedViewport.x + lockedViewport.width / 2) / pr;
  700. const offsetY = (lockedViewport.y + lockedViewport.height / 2) / pr;
  701. cross.style.width = `${crossWidth}px`;
  702. cross.style.left = `calc(${offsetX}px - ${crossWidth / 2}px)`;
  703. cross.style.bottom = `calc(${offsetY}px - ${crossWidth / 2}px)`;
  704. }
  705. }
  706. return {
  707. get noScroll() { return noScroll; },
  708. set noScroll(value: boolean) { noScroll = value; },
  709. get noContextMenu() { return noContextMenu; },
  710. set noContextMenu(value: boolean) { noContextMenu = value; },
  711. get width() { return width; },
  712. get height() { return height; },
  713. get pixelRatio() { return pixelRatio(); },
  714. get pointerLock() { return isLocked; },
  715. ...events,
  716. requestPointerLock: (viewport: Viewport) => {
  717. lockedViewport = viewport;
  718. if (!isLocked) {
  719. element.requestPointerLock();
  720. }
  721. },
  722. exitPointerLock: () => {
  723. if (isLocked) {
  724. element.ownerDocument.exitPointerLock();
  725. }
  726. },
  727. dispose
  728. };
  729. }
  730. }
  731. // Adapted from https://stackoverflow.com/a/30134826
  732. // License: https://creativecommons.org/licenses/by-sa/3.0/
  733. function normalizeWheel(event: any) {
  734. // Reasonable defaults
  735. const PIXEL_STEP = 10;
  736. const LINE_HEIGHT = 40;
  737. const PAGE_HEIGHT = 800;
  738. let spinX = 0, spinY = 0,
  739. dx = 0, dy = 0, dz = 0; // pixelX, pixelY, pixelZ
  740. // Legacy
  741. if ('detail' in event) { spinY = event.detail; }
  742. if ('wheelDelta' in event) { spinY = -event.wheelDelta / 120; }
  743. if ('wheelDeltaY' in event) { spinY = -event.wheelDeltaY / 120; }
  744. if ('wheelDeltaX' in event) { spinX = -event.wheelDeltaX / 120; }
  745. // side scrolling on FF with DOMMouseScroll
  746. if ('axis' in event && event.axis === event.HORIZONTAL_AXIS) {
  747. spinX = spinY;
  748. spinY = 0;
  749. }
  750. dx = spinX * PIXEL_STEP;
  751. dy = spinY * PIXEL_STEP;
  752. if ('deltaY' in event) { dy = event.deltaY; }
  753. if ('deltaX' in event) { dx = event.deltaX; }
  754. if ('deltaZ' in event) { dz = event.deltaZ; }
  755. if ((dx || dy || dz) && event.deltaMode) {
  756. if (event.deltaMode === 1) { // delta in LINE units
  757. dx *= LINE_HEIGHT;
  758. dy *= LINE_HEIGHT;
  759. dz *= LINE_HEIGHT;
  760. } else { // delta in PAGE units
  761. dx *= PAGE_HEIGHT;
  762. dy *= PAGE_HEIGHT;
  763. dz *= PAGE_HEIGHT;
  764. }
  765. }
  766. // Fall-back if spin cannot be determined
  767. if (dx && !spinX) { spinX = (dx < 1) ? -1 : 1; }
  768. if (dy && !spinY) { spinY = (dy < 1) ? -1 : 1; }
  769. return { spinX, spinY, dx, dy, dz };
  770. }
  771. export { InputObserver };