You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

pointer-tracker.ts 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  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 { Trackable, TrackerResult, TrackerOutput, Tracker } from '../tracker';
  25. import { PointerSource } from '../../sources/pointer-source';
  26. import { Vector2 } from '../../geometry/vector2';
  27. import { Utils, Nullable } from '../../utils/utils';
  28. import { IllegalOperationError } from '../../utils/errors';
  29. import { Session } from '../../core/session';
  30. import { Viewport } from '../../core/viewport';
  31. /**
  32. * The possible phases of a TrackablePointer
  33. */
  34. export type TrackablePointerPhase = 'began' | 'moved' | 'stationary' | 'ended' | 'canceled';
  35. /**
  36. * A trackable representing an instance of pointer-based input such as a mouse
  37. * click or a touch on the screen
  38. */
  39. export interface TrackablePointer extends Trackable
  40. {
  41. /** a unique identifier assigned to this trackable */
  42. readonly id: number;
  43. /** the phase of the trackable */
  44. readonly phase: TrackablePointerPhase;
  45. /** current position in normalized coordinates [-1,1]x[-1,1] */
  46. readonly position: Vector2;
  47. /** the position delta since the last frame */
  48. readonly deltaPosition: Vector2;
  49. /** the first position of this trackable, given in normalized coordinates */
  50. readonly initialPosition: Vector2;
  51. /** the current velocity, given in normalized coordinates per second */
  52. readonly velocity: Vector2;
  53. /** whether or not this is the primary pointer for this type */
  54. readonly isPrimary: boolean;
  55. /** the type of the originating device; typically "mouse", "touch" or "pen" */
  56. readonly type: string;
  57. }
  58. /**
  59. * A result of a PointerTracker. It's meant to be consumed by the user/application
  60. */
  61. export interface PointerTrackerResult extends TrackerResult
  62. {
  63. /** the tracker that generated this result */
  64. readonly tracker: PointerTracker;
  65. /** the trackables */
  66. readonly trackables: TrackablePointer[];
  67. }
  68. /**
  69. * The output of a PointerTracker in a particular Frame of a Session
  70. */
  71. export interface PointerTrackerOutput extends TrackerOutput
  72. {
  73. /** tracker result to be consumed by the user */
  74. readonly exports: PointerTrackerResult;
  75. }
  76. /** Convert event type to trackable pointer phase */
  77. const EVENTTYPE2PHASE: Record<string, TrackablePointerPhase> = {
  78. 'pointerdown': 'began',
  79. 'pointerup': 'ended',
  80. 'pointermove': 'moved',
  81. 'pointercancel': 'canceled',
  82. 'pointerleave': 'ended',
  83. 'pointerenter': 'began',
  84. };
  85. /**
  86. * A tracker of pointer-based input such as mouse, touch or pen
  87. */
  88. export class PointerTracker implements Tracker
  89. {
  90. /** the source of data */
  91. private _source: Nullable<PointerSource>;
  92. /** the viewport */
  93. private _viewport: Nullable<Viewport>;
  94. /** active pointers */
  95. private _activePointers: Map<number, TrackablePointer>;
  96. /** new pointers */
  97. private _newPointers: Map<number, TrackablePointer>;
  98. /** last output */
  99. private _lastOutput: PointerTrackerOutput;
  100. /** time of the last update */
  101. private _lastUpdateTime: DOMHighResTimeStamp;
  102. /**
  103. * Constructor
  104. */
  105. constructor()
  106. {
  107. this._source = null;
  108. this._viewport = null;
  109. this._activePointers = new Map();
  110. this._newPointers = new Map();
  111. this._lastOutput = this._generateOutput();
  112. this._lastUpdateTime = Number.POSITIVE_INFINITY;
  113. }
  114. /**
  115. * The type of the tracker
  116. */
  117. get type(): string
  118. {
  119. return 'pointer-tracker';
  120. }
  121. /**
  122. * Initialize the tracker
  123. * @param session
  124. * @returns a promise that is resolved as soon as the tracker is initialized
  125. * @internal
  126. */
  127. _init(session: Session): SpeedyPromise<void>
  128. {
  129. Utils.log('Initializing PointerTracker...');
  130. // set the viewport
  131. this._viewport = session.viewport;
  132. // find the pointer source
  133. for(const source of session.sources) {
  134. if(source._type == 'pointer-source') {
  135. this._source = source as PointerSource;
  136. break;
  137. }
  138. }
  139. if(this._source === null)
  140. return Speedy.Promise.reject(new IllegalOperationError('A PointerTracker expects a PointerSource'));
  141. // link the pointer source to the viewport
  142. this._source._setViewport(this._viewport);
  143. // done!
  144. return Speedy.Promise.resolve();
  145. }
  146. /**
  147. * Release the tracker
  148. * @returns a promise that is resolved as soon as the tracker is released
  149. * @internal
  150. */
  151. _release(): SpeedyPromise<void>
  152. {
  153. this._source = null;
  154. this._viewport = null;
  155. this._activePointers.clear();
  156. this._newPointers.clear();
  157. return Speedy.Promise.resolve();
  158. }
  159. /**
  160. * Update the tracker (update cycle)
  161. * @returns a promise that is resolved as soon as the tracker is updated
  162. * @internal
  163. */
  164. _update(): SpeedyPromise<void>
  165. {
  166. const canvas = this._viewport!.canvas;
  167. const rect = canvas.getBoundingClientRect(); // may be different in different frames!
  168. // find the time between this and the last update of this tracker
  169. const deltaTime = this._updateTime();
  170. const inverseDeltaTime = (deltaTime > 1e-5) ? 1 / deltaTime : 60; // 1/dt = 1 / (1/60) with 60 fps
  171. // remove inactive trackables from the previous frame (update cycle)
  172. const inactiveTrackables = this._findUnwantedTrackables();
  173. for(let i = inactiveTrackables.length - 1; i >= 0; i--)
  174. this._activePointers.delete(inactiveTrackables[i].id);
  175. // make all active trackables stationary
  176. this._activePointers.forEach((trackable, id) => {
  177. this._activePointers.set(id, Object.assign({}, trackable, {
  178. phase: 'stationary'
  179. }));
  180. });
  181. // consume events
  182. let event: Nullable<PointerEvent>;
  183. while((event = this._source!._consume()) !== null) {
  184. // sanity check
  185. if(event.target !== canvas)
  186. return Speedy.Promise.reject(new IllegalOperationError('Invalid PointerEvent target ' + event.target));
  187. else if(!EVENTTYPE2PHASE.hasOwnProperty(event.type))
  188. return Speedy.Promise.reject(new IllegalOperationError('Invalid PointerEvent type ' + event.type));
  189. // determine the ID
  190. // XXX different hardware devices acting simultaneously may produce
  191. // events with the same pointerId - handling this seems overkill?
  192. const id = event.pointerId;
  193. // determine the previous states, if any, of the trackable
  194. const previous = this._activePointers.get(id); // state in the previous frame
  195. const current = this._newPointers.get(id); // previous state in the current frame
  196. // determine the phase
  197. const phase = EVENTTYPE2PHASE[event.type];
  198. // new trackables always begin with a pointerdown event,
  199. // or with a pointerenter event having buttons pressed
  200. // (example: a mousemove without a previous mousedown isn't tracked)
  201. if(!(event.type == 'pointerdown' || (event.type == 'pointerenter' && event.buttons > 0))) {
  202. if(!previous && !current)
  203. continue; // discard event
  204. }
  205. else if(previous) {
  206. // discard a 'began' after another 'began'
  207. continue;
  208. }
  209. else if(event.button != 0 && event.pointerType == 'mouse') {
  210. // require left mouse click
  211. continue;
  212. }
  213. // discard event if 'began' and 'ended' happened in the same frame
  214. // (difficult to reproduce, but it can be done ;)
  215. if(!previous) {
  216. if(phase == 'ended' || phase == 'canceled') {
  217. this._newPointers.delete(id);
  218. continue;
  219. }
  220. }
  221. // what if we receive 'began' after 'ended' in the same frame?
  222. else if(phase == 'began' && current) {
  223. if(current.phase == 'ended' || current.phase == 'canceled') {
  224. this._newPointers.delete(id);
  225. continue;
  226. }
  227. }
  228. // more special rules
  229. switch(event.type) {
  230. case 'pointermove':
  231. if(event.buttons == 0 || current?.phase == 'began')
  232. continue;
  233. break;
  234. case 'pointerenter':
  235. if(event.buttons == 0 || previous?.phase == 'began' || current?.phase == 'began')
  236. continue;
  237. break;
  238. }
  239. // determine the current position
  240. const absX = event.pageX - (rect.left + window.scrollX);
  241. const absY = event.pageY - (rect.top + window.scrollY);
  242. const relX = 2 * absX / rect.width - 1; // convert to [-1,1]
  243. const relY = -(2 * absY / rect.height - 1); // flip Y axis
  244. const position = new Vector2(relX, relY);
  245. // determine the position delta
  246. const deltaPosition = !previous ? Vector2.Zero() :
  247. position._clone()._subtract(previous.position);
  248. // determine the initial position
  249. const initialPosition = previous ? previous.initialPosition :
  250. Object.freeze(position._clone());
  251. // determine the velocity
  252. const velocity = deltaPosition._clone()._scale(inverseDeltaTime);
  253. // determine whether or not this is the primary pointer for this type
  254. const isPrimary = event.isPrimary;
  255. // determine the type of the originating device
  256. const type = event.pointerType;
  257. // we create new trackable instances on each frame;
  258. // these will be exported and consumed by the user
  259. this._newPointers.set(id, { id, phase, position, deltaPosition, initialPosition, velocity, isPrimary, type });
  260. }
  261. // update trackables
  262. this._newPointers.forEach((trackable, id) => this._activePointers.set(id, trackable));
  263. this._newPointers.clear();
  264. // generate output
  265. this._lastOutput = this._generateOutput();
  266. // test
  267. //console.log(JSON.stringify(this._lastOutput.exports.trackables, null, 4));
  268. // done!
  269. return Speedy.Promise.resolve();
  270. }
  271. /**
  272. * Output of the last frame
  273. * @internal
  274. */
  275. get _output(): PointerTrackerOutput
  276. {
  277. return this._lastOutput;
  278. }
  279. /**
  280. * Stats info
  281. * @internal
  282. */
  283. get _stats(): string
  284. {
  285. const n = this._activePointers.size;
  286. const s = n != 1 ? 's' : '';
  287. return n + ' pointer' + s;
  288. }
  289. /**
  290. * Generate tracker output
  291. * @returns a new PointerTrackerOutput object
  292. */
  293. private _generateOutput(): PointerTrackerOutput
  294. {
  295. const trackables: TrackablePointer[] = [];
  296. this._activePointers.forEach(trackable => trackables.push(trackable));
  297. return {
  298. exports: {
  299. tracker: this,
  300. trackables: trackables
  301. }
  302. };
  303. }
  304. /**
  305. * Find trackables to remove
  306. * @returns a list of trackables to remove
  307. */
  308. private _findUnwantedTrackables(): TrackablePointer[]
  309. {
  310. const trackables: TrackablePointer[] = [];
  311. this._activePointers.forEach(trackable => {
  312. if(trackable.phase == 'ended' || trackable.phase == 'canceled')
  313. trackables.push(trackable);
  314. });
  315. return trackables;
  316. }
  317. /**
  318. * Update the time
  319. * @returns delta time in seconds
  320. */
  321. private _updateTime(): DOMHighResTimeStamp
  322. {
  323. const now = performance.now() * 0.001;
  324. if(this._lastUpdateTime > now)
  325. this._lastUpdateTime = now;
  326. const prev = this._lastUpdateTime;
  327. this._lastUpdateTime = now;
  328. return now - prev;
  329. }
  330. }