Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

pointer-tracker.ts 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  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. /**
  77. * Constructor
  78. */
  79. constructor()
  80. {
  81. this._source = null;
  82. this._viewport = null;
  83. this._activePointers = new Map();
  84. this._newPointers = new Map();
  85. this._previousOutput = this._generateOutput();
  86. this._previousUpdateTime = Number.POSITIVE_INFINITY;
  87. }
  88. /**
  89. * The type of the tracker
  90. */
  91. get type(): string
  92. {
  93. return 'pointer-tracker';
  94. }
  95. /**
  96. * Initialize the tracker
  97. * @param session
  98. * @returns a promise that is resolved as soon as the tracker is initialized
  99. * @internal
  100. */
  101. _init(session: Session): SpeedyPromise<void>
  102. {
  103. Utils.log('Initializing PointerTracker...');
  104. // set the viewport
  105. this._viewport = session.viewport;
  106. // find the pointer source
  107. for(const source of session.sources) {
  108. if(source._type == 'pointer-source') {
  109. this._source = source as PointerSource;
  110. break;
  111. }
  112. }
  113. if(this._source === null)
  114. return Speedy.Promise.reject(new IllegalOperationError('A PointerTracker expects a PointerSource'));
  115. // link the pointer source to the viewport
  116. this._source._setViewport(this._viewport);
  117. // done!
  118. return Speedy.Promise.resolve();
  119. }
  120. /**
  121. * Release the tracker
  122. * @returns a promise that is resolved as soon as the tracker is released
  123. * @internal
  124. */
  125. _release(): SpeedyPromise<void>
  126. {
  127. this._source = null;
  128. this._viewport = null;
  129. this._activePointers.clear();
  130. this._newPointers.clear();
  131. return Speedy.Promise.resolve();
  132. }
  133. /**
  134. * Update the tracker (update cycle)
  135. * @returns a promise that is resolved as soon as the tracker is updated
  136. * @internal
  137. */
  138. _update(): SpeedyPromise<void>
  139. {
  140. const canvas = this._viewport!.canvas;
  141. const rect = canvas.getBoundingClientRect(); // may be different in different frames!
  142. // find the time between this and the previous update of this tracker
  143. const deltaTime = this._updateTime();
  144. const inverseDeltaTime = (deltaTime > 1e-5) ? 1 / deltaTime : 60; // 1/dt = 1 / (1/60) with 60 fps
  145. // remove inactive trackables from the previous frame (update cycle)
  146. const inactiveTrackables = this._findUnwantedTrackables();
  147. for(let i = inactiveTrackables.length - 1; i >= 0; i--)
  148. this._activePointers.delete(inactiveTrackables[i].id);
  149. // make all active trackables stationary
  150. this._activePointers.forEach((trackable, id) => {
  151. this._activePointers.set(id, Object.assign({}, trackable, {
  152. phase: 'stationary'
  153. }));
  154. });
  155. // consume events
  156. let event: Nullable<PointerEvent>;
  157. while((event = this._source!._consume()) !== null) {
  158. // sanity check
  159. if(event.target !== canvas)
  160. return Speedy.Promise.reject(new IllegalOperationError('Invalid PointerEvent target ' + event.target));
  161. else if(!EVENTTYPE2PHASE.hasOwnProperty(event.type))
  162. return Speedy.Promise.reject(new IllegalOperationError('Invalid PointerEvent type ' + event.type));
  163. // determine the ID
  164. // XXX different hardware devices acting simultaneously may produce
  165. // events with the same pointerId - handling this seems overkill?
  166. const id = event.pointerId;
  167. // determine the previous states, if any, of the trackable
  168. const previous = this._activePointers.get(id); // state in the previous frame
  169. const current = this._newPointers.get(id); // previous state in the current frame
  170. // determine the phase
  171. const phase = EVENTTYPE2PHASE[event.type];
  172. // new trackables always begin with a pointerdown event,
  173. // or with a pointerenter event having buttons pressed
  174. // (example: a mousemove without a previous mousedown isn't tracked)
  175. if(!(event.type == 'pointerdown' || (event.type == 'pointerenter' && event.buttons > 0))) {
  176. if(!previous && !current)
  177. continue; // discard event
  178. }
  179. else if(previous) {
  180. // discard a 'began' after another 'began'
  181. continue;
  182. }
  183. else if(event.button != 0 && event.pointerType == 'mouse') {
  184. // require left mouse click
  185. continue;
  186. }
  187. // discard event if 'began' and 'ended' happened in the same frame
  188. // (difficult to reproduce, but it can be done ;)
  189. if(!previous) {
  190. if(phase == 'ended' || phase == 'canceled') {
  191. this._newPointers.delete(id);
  192. continue;
  193. }
  194. }
  195. // what if we receive 'began' after 'ended' in the same frame?
  196. else if(phase == 'began' && current) {
  197. if(current.phase == 'ended' || current.phase == 'canceled') {
  198. this._newPointers.delete(id);
  199. continue;
  200. }
  201. }
  202. // more special rules
  203. switch(event.type) {
  204. case 'pointermove':
  205. if(event.buttons == 0 || current?.phase == 'began')
  206. continue;
  207. break;
  208. case 'pointerenter':
  209. if(event.buttons == 0 || previous?.phase == 'began' || current?.phase == 'began')
  210. continue;
  211. break;
  212. }
  213. // determine the current position
  214. const absX = event.pageX - (rect.left + window.scrollX);
  215. const absY = event.pageY - (rect.top + window.scrollY);
  216. const relX = 2 * absX / rect.width - 1; // convert to [-1,1]
  217. const relY = -(2 * absY / rect.height - 1); // flip Y axis
  218. const position = new Vector2(relX, relY);
  219. // determine the position delta
  220. const deltaPosition = !previous ? Vector2.Zero() :
  221. position._clone()._subtract(previous.position);
  222. // determine the initial position
  223. const initialPosition = previous ? previous.initialPosition :
  224. Object.freeze(position._clone());
  225. // determine the velocity
  226. const velocity = deltaPosition._clone()._scale(inverseDeltaTime);
  227. // determine the elapsed time since the tracking began
  228. const elapsedTime = previous ? previous.elapsedTime + deltaTime : 0;
  229. // determine whether or not this is the primary pointer for this type
  230. const isPrimary = event.isPrimary;
  231. // determine the type of the originating device
  232. const type = event.pointerType;
  233. // we create new trackable instances on each frame;
  234. // these will be exported and consumed by the user
  235. this._newPointers.set(id, { id, phase, position, deltaPosition, initialPosition, velocity, elapsedTime, isPrimary, type });
  236. }
  237. // update trackables
  238. this._newPointers.forEach((trackable, id) => this._activePointers.set(id, trackable));
  239. this._newPointers.clear();
  240. // generate output
  241. this._previousOutput = this._generateOutput();
  242. // test
  243. //console.log(JSON.stringify(this._prevOutput.exports.trackables, null, 4));
  244. // done!
  245. return Speedy.Promise.resolve();
  246. }
  247. /**
  248. * Output of the previous frame
  249. * @internal
  250. */
  251. get _output(): PointerTrackerOutput
  252. {
  253. return this._previousOutput;
  254. }
  255. /**
  256. * Stats info
  257. * @internal
  258. */
  259. get _stats(): string
  260. {
  261. const n = this._activePointers.size;
  262. const s = n != 1 ? 's' : '';
  263. return n + ' pointer' + s;
  264. }
  265. /**
  266. * Generate tracker output
  267. * @returns a new PointerTrackerOutput object
  268. */
  269. private _generateOutput(): PointerTrackerOutput
  270. {
  271. const trackables: TrackablePointer[] = [];
  272. this._activePointers.forEach(trackable => trackables.push(trackable));
  273. return {
  274. exports: {
  275. tracker: this,
  276. trackables: trackables
  277. }
  278. };
  279. }
  280. /**
  281. * Find trackables to remove
  282. * @returns a list of trackables to remove
  283. */
  284. private _findUnwantedTrackables(): TrackablePointer[]
  285. {
  286. const trackables: TrackablePointer[] = [];
  287. this._activePointers.forEach(trackable => {
  288. if(trackable.phase == 'ended' || trackable.phase == 'canceled')
  289. trackables.push(trackable);
  290. });
  291. return trackables;
  292. }
  293. /**
  294. * Update the time
  295. * @returns delta time in seconds
  296. */
  297. private _updateTime(): DOMHighResTimeStamp
  298. {
  299. const now = performance.now() * 0.001;
  300. if(this._previousUpdateTime > now)
  301. this._previousUpdateTime = now;
  302. const prev = this._previousUpdateTime;
  303. this._previousUpdateTime = now;
  304. return now - prev;
  305. }
  306. }