Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

pointer-tracker.ts 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. /*
  2. * encantar.js
  3. * GPU-accelerated Augmented Reality for the web
  4. * Copyright (C) 2022-2024 Alexandre Martins <alemartf(at)gmail.com>
  5. *
  6. * This program is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Lesser General Public License as published
  8. * by the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. *
  19. * pointer-tracker.ts
  20. * Tracker of pointer-based input
  21. */
  22. import Speedy from 'speedy-vision';
  23. import { SpeedyPromise } from 'speedy-vision/types/core/speedy-promise';
  24. import { TrackerResult, TrackerOutput, Tracker } from '../tracker';
  25. import { TrackablePointer, TrackablePointerPhase } from './trackable-pointer';
  26. import { PointerSource } from '../../sources/pointer-source';
  27. import { Vector2 } from '../../geometry/vector2';
  28. import { Utils, Nullable } from '../../utils/utils';
  29. import { IllegalOperationError } from '../../utils/errors';
  30. import { Session } from '../../core/session';
  31. import { Viewport } from '../../core/viewport';
  32. /**
  33. * A result of a PointerTracker. It's meant to be consumed by the user/application
  34. */
  35. export interface PointerTrackerResult extends TrackerResult
  36. {
  37. /** the tracker that generated this result */
  38. readonly tracker: PointerTracker;
  39. /** the trackables */
  40. readonly trackables: TrackablePointer[];
  41. }
  42. /**
  43. * The output of a PointerTracker in a particular Frame of a Session
  44. */
  45. export interface PointerTrackerOutput extends TrackerOutput
  46. {
  47. /** tracker result to be consumed by the user */
  48. readonly exports: PointerTrackerResult;
  49. }
  50. /** Convert event type to trackable pointer phase */
  51. const EVENTTYPE2PHASE: Record<string, TrackablePointerPhase> = {
  52. 'pointerdown': 'began',
  53. 'pointerup': 'ended',
  54. 'pointermove': 'moved',
  55. 'pointercancel': 'canceled',
  56. 'pointerleave': 'ended',
  57. 'pointerenter': 'began',
  58. };
  59. /**
  60. * A tracker of pointer-based input such as mouse, touch or pen
  61. */
  62. export class PointerTracker implements Tracker
  63. {
  64. /** the source of data */
  65. private _source: Nullable<PointerSource>;
  66. /** the viewport */
  67. private _viewport: Nullable<Viewport>;
  68. /** active pointers */
  69. private _activePointers: Map<number, TrackablePointer>;
  70. /** new pointers */
  71. private _newPointers: Map<number, TrackablePointer>;
  72. /** previous output */
  73. private _previousOutput: PointerTrackerOutput;
  74. /** time of the previous update */
  75. private _previousUpdateTime: DOMHighResTimeStamp;
  76. /** helper flag */
  77. private _wantToReset: boolean;
  78. /**
  79. * Constructor
  80. */
  81. constructor()
  82. {
  83. this._source = null;
  84. this._viewport = null;
  85. this._activePointers = new Map();
  86. this._newPointers = new Map();
  87. this._previousOutput = this._generateOutput();
  88. this._previousUpdateTime = Number.POSITIVE_INFINITY;
  89. this._wantToReset = false;
  90. this._resetInTheNextUpdate = this._resetInTheNextUpdate.bind(this);
  91. }
  92. /**
  93. * The type of the tracker
  94. */
  95. get type(): string
  96. {
  97. return 'pointer-tracker';
  98. }
  99. /**
  100. * Initialize the tracker
  101. * @param session
  102. * @returns a promise that is resolved as soon as the tracker is initialized
  103. * @internal
  104. */
  105. _init(session: Session): SpeedyPromise<void>
  106. {
  107. Utils.log('Initializing PointerTracker...');
  108. // set the viewport
  109. this._viewport = session.viewport;
  110. // find the pointer source
  111. for(const source of session.sources) {
  112. if(source._type == 'pointer-source') {
  113. this._source = source as PointerSource;
  114. break;
  115. }
  116. }
  117. if(this._source === null)
  118. return Speedy.Promise.reject(new IllegalOperationError('A PointerTracker expects a PointerSource'));
  119. // link the pointer source to the viewport
  120. this._source._setViewport(this._viewport);
  121. // reset trackables
  122. document.addEventListener('visibilitychange', this._resetInTheNextUpdate);
  123. // done!
  124. return Speedy.Promise.resolve();
  125. }
  126. /**
  127. * Release the tracker
  128. * @returns a promise that is resolved as soon as the tracker is released
  129. * @internal
  130. */
  131. _release(): SpeedyPromise<void>
  132. {
  133. this._source = null;
  134. this._viewport = null;
  135. this._activePointers.clear();
  136. this._newPointers.clear();
  137. document.removeEventListener('visibilitychange', this._resetInTheNextUpdate);
  138. return Speedy.Promise.resolve();
  139. }
  140. /**
  141. * Update the tracker (update cycle)
  142. * @returns a promise that is resolved as soon as the tracker is updated
  143. * @internal
  144. */
  145. _update(): SpeedyPromise<void>
  146. {
  147. const canvas = this._viewport!.canvas;
  148. const rect = canvas.getBoundingClientRect(); // may be different in different frames!
  149. // find the time between this and the previous update of this tracker
  150. const deltaTime = this._updateTime();
  151. const inverseDeltaTime = (deltaTime > 1e-5) ? 1 / deltaTime : 60; // 1/dt = 1 / (1/60) with 60 fps
  152. // remove inactive trackables from the previous frame (update cycle)
  153. const inactiveTrackables = this._findInactiveTrackables();
  154. for(let i = inactiveTrackables.length - 1; i >= 0; i--)
  155. this._activePointers.delete(inactiveTrackables[i].id);
  156. // make all active trackables stationary
  157. this._updateAllTrackables({
  158. phase: 'stationary',
  159. velocity: Vector2.ZERO,
  160. deltaPosition: Vector2.ZERO
  161. });
  162. // want to reset?
  163. if(this._wantToReset) {
  164. this._reset();
  165. this._wantToReset = false;
  166. }
  167. // consume events
  168. let event: Nullable<PointerEvent>;
  169. while((event = this._source!._consume()) !== null) {
  170. // sanity check
  171. if(event.target !== canvas)
  172. return Speedy.Promise.reject(new IllegalOperationError('Invalid PointerEvent target ' + event.target));
  173. else if(!EVENTTYPE2PHASE.hasOwnProperty(event.type))
  174. return Speedy.Promise.reject(new IllegalOperationError('Invalid PointerEvent type ' + event.type));
  175. // determine the ID
  176. // XXX different hardware devices acting simultaneously may produce
  177. // events with the same pointerId - handling this seems overkill?
  178. const id = event.pointerId;
  179. // determine the previous states, if any, of the trackable
  180. const previous = this._activePointers.get(id); // state in the previous frame
  181. const current = this._newPointers.get(id); // previous state in the current frame
  182. // determine the phase
  183. const phase = EVENTTYPE2PHASE[event.type];
  184. // new trackables always begin with a pointerdown event,
  185. // or with a pointerenter event having buttons pressed
  186. // (example: a mousemove without a previous mousedown isn't tracked)
  187. if(!(event.type == 'pointerdown' || (event.type == 'pointerenter' && event.buttons > 0))) {
  188. if(!previous && !current)
  189. continue; // discard event
  190. }
  191. else if(previous) {
  192. // discard a 'began' after another 'began'
  193. continue;
  194. }
  195. else if(event.button != 0 && event.pointerType == 'mouse') {
  196. // require left mouse click
  197. continue;
  198. }
  199. // discard event if 'began' and 'ended' happened in the same frame
  200. // (difficult to reproduce, but it can be done ;)
  201. if(!previous) {
  202. if(phase == 'ended' || phase == 'canceled') {
  203. this._newPointers.delete(id);
  204. continue;
  205. }
  206. }
  207. // what if we receive 'began' after 'ended' in the same frame?
  208. else if(phase == 'began' && current) {
  209. if(current.phase == 'ended' || current.phase == 'canceled') {
  210. this._newPointers.delete(id);
  211. continue;
  212. }
  213. }
  214. // discard previously canceled pointers (e.g., with a visibilitychange event)
  215. if(previous?.phase == 'canceled')
  216. continue;
  217. // more special rules
  218. switch(event.type) {
  219. case 'pointermove':
  220. if(event.buttons == 0 || current?.phase == 'began')
  221. continue;
  222. break;
  223. case 'pointerenter':
  224. if(event.buttons == 0 || previous?.phase == 'began' || current?.phase == 'began')
  225. continue;
  226. break;
  227. case 'pointercancel': // purge everything
  228. this._reset();
  229. this._newPointers.clear();
  230. continue;
  231. }
  232. // determine the current position
  233. const absX = event.pageX - (rect.left + window.scrollX);
  234. const absY = event.pageY - (rect.top + window.scrollY);
  235. const relX = 2 * absX / rect.width - 1; // convert to [-1,1]
  236. const relY = -(2 * absY / rect.height - 1); // flip Y axis
  237. const position = new Vector2(relX, relY);
  238. // determine the position delta
  239. const deltaPosition = !previous ? Vector2.ZERO :
  240. position._clone()._subtract(previous.position);
  241. // determine the initial position
  242. const initialPosition = previous ? previous.initialPosition :
  243. Object.freeze(position._clone());
  244. // determine the velocity
  245. const velocity = deltaPosition._clone()._scale(inverseDeltaTime);
  246. // determine the elapsed time since the tracking began
  247. const elapsedTime = previous ? previous.elapsedTime + deltaTime : 0;
  248. // determine whether or not this is the primary pointer for this type
  249. const isPrimary = event.isPrimary;
  250. // determine the type of the originating device
  251. const kind = event.pointerType;
  252. // we create new trackable instances on each frame;
  253. // these will be exported and consumed by the user
  254. this._newPointers.set(id, { id, phase, position, deltaPosition, initialPosition, velocity, elapsedTime, isPrimary, kind });
  255. }
  256. // update trackables
  257. this._newPointers.forEach((trackable, id) => this._activePointers.set(id, trackable));
  258. this._newPointers.clear();
  259. this._advanceAllStationaryTrackables(deltaTime);
  260. // generate output
  261. this._previousOutput = this._generateOutput();
  262. // test
  263. //console.log(JSON.stringify(this._prevOutput.exports.trackables, null, 4));
  264. // done!
  265. return Speedy.Promise.resolve();
  266. }
  267. /**
  268. * Output of the previous frame
  269. * @internal
  270. */
  271. get _output(): PointerTrackerOutput
  272. {
  273. return this._previousOutput;
  274. }
  275. /**
  276. * Stats info
  277. * @internal
  278. */
  279. get _stats(): string
  280. {
  281. const n = this._activePointers.size;
  282. const s = n != 1 ? 's' : '';
  283. return n + ' pointer' + s;
  284. }
  285. /**
  286. * Generate tracker output
  287. * @returns a new PointerTrackerOutput object
  288. */
  289. private _generateOutput(): PointerTrackerOutput
  290. {
  291. const trackables: TrackablePointer[] = [];
  292. this._activePointers.forEach(trackable => trackables.push(trackable));
  293. return {
  294. exports: {
  295. tracker: this,
  296. trackables: this._sortTrackables(trackables)
  297. }
  298. };
  299. }
  300. /**
  301. * Update all active pointers
  302. * @param fields
  303. */
  304. private _updateAllTrackables(fields: Partial<TrackablePointer>): void
  305. {
  306. this._activePointers.forEach((trackable, id) => {
  307. this._activePointers.set(id, Object.assign({}, trackable, fields));
  308. });
  309. }
  310. /**
  311. * Advance the elapsed time of all stationary pointers
  312. * @param deltaTime
  313. */
  314. private _advanceAllStationaryTrackables(deltaTime: number): void
  315. {
  316. this._activePointers.forEach((trackable, id) => {
  317. if(trackable.phase == 'stationary') {
  318. (trackable as any).elapsedTime += deltaTime;
  319. /*
  320. this._activePointers.set(id, Object.assign({}, trackable, {
  321. elapsedTime: trackable.elapsedTime + deltaTime
  322. }));
  323. */
  324. }
  325. });
  326. }
  327. /**
  328. * Cancel all active pointers and consume all events
  329. * @param deltaTime
  330. */
  331. private _reset(): void
  332. {
  333. // cancel all active pointers
  334. this._updateAllTrackables({
  335. phase: 'canceled',
  336. velocity: Vector2.ZERO,
  337. deltaPosition: Vector2.ZERO
  338. });
  339. // consume all events
  340. while(this._source!._consume() !== null);
  341. }
  342. /**
  343. * Reset in the next update of the tracker
  344. */
  345. private _resetInTheNextUpdate(): void
  346. {
  347. this._wantToReset = true;
  348. }
  349. /**
  350. * As a convenience, let's make sure that a primary pointer, if any exists,
  351. * is at the beginning of the trackables array
  352. * @param trackables
  353. * @returns sorted trackables
  354. */
  355. private _sortTrackables(trackables: TrackablePointer[]): TrackablePointer[]
  356. {
  357. /*
  358. Note: the browser may not report a new unique pointer (phase: "began")
  359. as primary. This logic makes trackables[0] primary, or sort of primary.
  360. Behavior on Chrome 130 on Android: when moving multiple touch points,
  361. remove focus from the browser. Touch points will be canceled as
  362. expected. When touching the screen again with a single finger, the
  363. (only one) registered pointer will not be primary. That's undesirable.
  364. Touching the screen again with multiple fingers (none will be primary),
  365. and then releasing them, will restore the desired behavior.
  366. */
  367. // nothing to do
  368. if(trackables.length <= 1 || trackables[0].isPrimary)
  369. return trackables;
  370. // find a primary pointer and swap
  371. for(let j = 1; j < trackables.length; j++) {
  372. if(trackables[j].isPrimary) {
  373. const primary = trackables[j];
  374. trackables[j] = trackables[0];
  375. trackables[0] = primary;
  376. break;
  377. }
  378. }
  379. // done!
  380. return trackables;
  381. }
  382. /**
  383. * Find trackables to remove
  384. * @returns a list of trackables to remove
  385. */
  386. private _findInactiveTrackables(): TrackablePointer[]
  387. {
  388. const trackables: TrackablePointer[] = [];
  389. this._activePointers.forEach(trackable => {
  390. if(trackable.phase == 'ended' || trackable.phase == 'canceled')
  391. trackables.push(trackable);
  392. });
  393. return trackables;
  394. }
  395. /**
  396. * Update the time
  397. * @returns delta time in seconds
  398. */
  399. private _updateTime(): DOMHighResTimeStamp
  400. {
  401. const now = performance.now() * 0.001;
  402. if(this._previousUpdateTime > now)
  403. this._previousUpdateTime = now;
  404. const prev = this._previousUpdateTime;
  405. this._previousUpdateTime = now;
  406. return now - prev;
  407. }
  408. }