input-observer.ts 33 KB

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