• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

excaliburjs / Excalibur / 11647275945

03 Nov 2024 01:14AM UTC coverage: 90.198% (-0.2%) from 90.374%
11647275945

push

github

eonarheim
docs: fix version header

5861 of 7457 branches covered (78.6%)

12837 of 14232 relevant lines covered (90.2%)

25251.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.3
/src/engine/Input/PointerEventReceiver.ts
1
import { Engine, ScrollPreventionMode } from '../Engine';
2
import { GlobalCoordinates } from '../Math/global-coordinates';
3
import { vec, Vector } from '../Math/vector';
4
import { PointerEvent } from './PointerEvent';
5
import { WheelEvent } from './WheelEvent';
6
import { PointerAbstraction } from './PointerAbstraction';
7

8
import { WheelDeltaMode } from './WheelDeltaMode';
9
import { PointerSystem } from './PointerSystem';
10
import { NativePointerButton } from './NativePointerButton';
11
import { PointerButton } from './PointerButton';
12
import { fail } from '../Util/Util';
13
import { PointerType } from './PointerType';
14
import { isCrossOriginIframe } from '../Util/IFrame';
15
import { EventEmitter, EventKey, Handler, Subscription } from '../EventEmitter';
16

17
export type NativePointerEvent = globalThis.PointerEvent;
18
export type NativeMouseEvent = globalThis.MouseEvent;
19
export type NativeTouchEvent = globalThis.TouchEvent;
20
export type NativeWheelEvent = globalThis.WheelEvent;
21

22
export type PointerEvents = {
23
  move: PointerEvent;
24
  down: PointerEvent;
25
  up: PointerEvent;
26
  wheel: WheelEvent;
27
};
28

29
export const PointerEvents = {
1✔
30
  Move: 'move',
31
  Down: 'down',
32
  Up: 'up',
33
  Wheel: 'wheel'
34
};
35

36
/**
37
 * Is this event a native touch event?
38
 */
39
function isTouchEvent(value: any): value is NativeTouchEvent {
40
  // Guard for Safari <= 13.1
41
  return globalThis.TouchEvent && value instanceof globalThis.TouchEvent;
329✔
42
}
43

44
/**
45
 * Is this event a native pointer event
46
 */
47
function isPointerEvent(value: any): value is NativePointerEvent {
48
  // Guard for Safari <= 13.1
49
  return globalThis.PointerEvent && value instanceof globalThis.PointerEvent;
329✔
50
}
51

52
export interface PointerInitOptions {
53
  grabWindowFocus?: boolean;
54
}
55

56
/**
57
 * The PointerEventProcessor is responsible for collecting all the events from the canvas and transforming them into GlobalCoordinates
58
 */
59
export class PointerEventReceiver {
60
  public events = new EventEmitter<PointerEvents>();
1,326✔
61
  public primary: PointerAbstraction = new PointerAbstraction();
1,326✔
62

63
  private _activeNativePointerIdsToNormalized = new Map<number, number>();
1,326✔
64
  public lastFramePointerCoords = new Map<number, GlobalCoordinates>();
1,326✔
65
  public currentFramePointerCoords = new Map<number, GlobalCoordinates>();
1,326✔
66

67
  public currentFramePointerDown = new Map<number, boolean>();
1,326✔
68
  public lastFramePointerDown = new Map<number, boolean>();
1,326✔
69

70
  public currentFrameDown: PointerEvent[] = [];
1,326✔
71
  public currentFrameUp: PointerEvent[] = [];
1,326✔
72
  public currentFrameMove: PointerEvent[] = [];
1,326✔
73
  public currentFrameCancel: PointerEvent[] = [];
1,326✔
74
  public currentFrameWheel: WheelEvent[] = [];
1,326✔
75

76
  private _enabled = true;
1,326✔
77

78
  constructor(
79
    public readonly target: GlobalEventHandlers & EventTarget,
1,326✔
80
    public engine: Engine
1,326✔
81
  ) {}
82

83
  public toggleEnabled(enabled: boolean) {
84
    this._enabled = enabled;
1,750✔
85
  }
86

87
  /**
88
   * Creates a new PointerEventReceiver with a new target and engine while preserving existing pointer event
89
   * handlers.
90
   * @param target
91
   * @param engine
92
   */
93
  public recreate(target: GlobalEventHandlers & EventTarget, engine: Engine) {
94
    const eventReceiver = new PointerEventReceiver(target, engine);
1✔
95
    eventReceiver.primary = this.primary;
1✔
96
    eventReceiver._pointers = this._pointers;
1✔
97
    return eventReceiver;
1✔
98
  }
99

100
  private _pointers: PointerAbstraction[] = [this.primary];
1,326✔
101
  /**
102
   * Locates a specific pointer by id, creates it if it doesn't exist
103
   * @param index
104
   */
105
  public at(index: number): PointerAbstraction {
106
    if (index >= this._pointers.length) {
87!
107
      // Ensure there is a pointer to retrieve
108
      for (let i = this._pointers.length - 1, max = index; i < max; i++) {
×
109
        this._pointers.push(new PointerAbstraction());
×
110
      }
111
    }
112
    return this._pointers[index];
87✔
113
  }
114

115
  /**
116
   * The number of pointers currently being tracked by excalibur
117
   */
118
  public count(): number {
119
    return this._pointers.length;
×
120
  }
121

122
  /**
123
   * Is the specified pointer id down this frame
124
   * @param pointerId
125
   */
126
  public isDown(pointerId: number) {
127
    if (!this._enabled) {
91!
128
      return false;
×
129
    }
130
    return this.currentFramePointerDown.get(pointerId) ?? false;
91✔
131
  }
132

133
  /**
134
   * Was the specified pointer id down last frame
135
   * @param pointerId
136
   */
137
  public wasDown(pointerId: number) {
138
    if (!this._enabled) {
35!
139
      return false;
×
140
    }
141
    return this.lastFramePointerDown.get(pointerId) ?? false;
35✔
142
  }
143

144
  /**
145
   * Whether the Pointer is currently dragging.
146
   */
147
  public isDragging(pointerId: number): boolean {
148
    if (!this._enabled) {
55!
149
      return false;
×
150
    }
151
    return this.isDown(pointerId);
55✔
152
  }
153

154
  /**
155
   * Whether the Pointer just started dragging.
156
   */
157
  public isDragStart(pointerId: number): boolean {
158
    if (!this._enabled) {
27!
159
      return false;
×
160
    }
161
    return this.isDown(pointerId) && !this.wasDown(pointerId);
27✔
162
  }
163

164
  /**
165
   * Whether the Pointer just ended dragging.
166
   */
167
  public isDragEnd(pointerId: number): boolean {
168
    if (!this._enabled) {
9!
169
      return false;
×
170
    }
171
    return !this.isDown(pointerId) && this.wasDown(pointerId);
9✔
172
  }
173

174
  public emit<TEventName extends EventKey<PointerEvents>>(eventName: TEventName, event: PointerEvents[TEventName]): void;
175
  public emit(eventName: string, event?: any): void;
176
  public emit<TEventName extends EventKey<PointerEvents> | string>(eventName: TEventName, event?: any): void {
177
    this.events.emit(eventName, event);
84✔
178
  }
179

180
  public on<TEventName extends EventKey<PointerEvents>>(eventName: TEventName, handler: Handler<PointerEvents[TEventName]>): Subscription;
181
  public on(eventName: string, handler: Handler<unknown>): Subscription;
182
  public on<TEventName extends EventKey<PointerEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription {
183
    return this.events.on(eventName, handler);
8✔
184
  }
185

186
  public once<TEventName extends EventKey<PointerEvents>>(eventName: TEventName, handler: Handler<PointerEvents[TEventName]>): Subscription;
187
  public once(eventName: string, handler: Handler<unknown>): Subscription;
188
  public once<TEventName extends EventKey<PointerEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription {
189
    return this.events.once(eventName, handler);
×
190
  }
191

192
  public off<TEventName extends EventKey<PointerEvents>>(eventName: TEventName, handler: Handler<PointerEvents[TEventName]>): void;
193
  public off(eventName: string, handler: Handler<unknown>): void;
194
  public off(eventName: string): void;
195
  public off<TEventName extends EventKey<PointerEvents> | string>(eventName: TEventName, handler?: Handler<any>): void {
196
    this.events.off(eventName, handler);
×
197
  }
198

199
  /**
200
   * Called internally by excalibur
201
   *
202
   * Updates the current frame pointer info and emits raw pointer events
203
   *
204
   * This does not emit events to entities, see PointerSystem
205
   */
206
  public update() {
207
    this.lastFramePointerDown = new Map(this.currentFramePointerDown);
3,332✔
208
    this.lastFramePointerCoords = new Map(this.currentFramePointerCoords);
3,332✔
209

210
    for (const event of this.currentFrameDown) {
3,332✔
211
      if (!event.active) {
45✔
212
        continue;
2✔
213
      }
214
      this.emit('down', event);
43✔
215
      const pointer = this.at(event.pointerId);
43✔
216
      pointer.emit('down', event);
43✔
217
      this.primary.emit('pointerdown', event);
43✔
218
    }
219

220
    for (const event of this.currentFrameUp) {
3,332✔
221
      if (!event.active) {
15✔
222
        continue;
1✔
223
      }
224
      this.emit('up', event);
14✔
225
      const pointer = this.at(event.pointerId);
14✔
226
      pointer.emit('up', event);
14✔
227
    }
228

229
    for (const event of this.currentFrameMove) {
3,332✔
230
      if (!event.active) {
20✔
231
        continue;
1✔
232
      }
233
      this.emit('move', event);
19✔
234
      const pointer = this.at(event.pointerId);
19✔
235
      pointer.emit('move', event);
19✔
236
    }
237

238
    for (const event of this.currentFrameCancel) {
3,332✔
239
      if (!event.active) {
2!
240
        continue;
×
241
      }
242
      this.emit('cancel', event);
2✔
243
      const pointer = this.at(event.pointerId);
2✔
244
      pointer.emit('cancel', event);
2✔
245
    }
246

247
    for (const event of this.currentFrameWheel) {
3,332✔
248
      if (!event.active) {
4✔
249
        continue;
1✔
250
      }
251
      this.emit('pointerwheel', event);
3✔
252
      this.emit('wheel', event);
3✔
253
      this.primary.emit('pointerwheel', event);
3✔
254
      this.primary.emit('wheel', event);
3✔
255
    }
256
  }
257

258
  /**
259
   * Clears the current frame event and pointer data
260
   */
261
  public clear() {
262
    for (const event of this.currentFrameUp) {
3,368✔
263
      this.currentFramePointerCoords.delete(event.pointerId);
15✔
264
      const ids = this._activeNativePointerIdsToNormalized.entries();
15✔
265
      for (const [native, normalized] of ids) {
15✔
266
        if (normalized === event.pointerId) {
11!
267
          this._activeNativePointerIdsToNormalized.delete(native);
11✔
268
        }
269
      }
270
    }
271
    this.currentFrameDown.length = 0;
3,368✔
272
    this.currentFrameUp.length = 0;
3,368✔
273
    this.currentFrameMove.length = 0;
3,368✔
274
    this.currentFrameCancel.length = 0;
3,368✔
275
    this.currentFrameWheel.length = 0;
3,368✔
276
  }
277

278
  private _boundHandle = this._handle.bind(this);
1,326✔
279
  private _boundWheel = this._handleWheel.bind(this);
1,326✔
280
  /**
281
   * Initializes the pointer event receiver so that it can start listening to native
282
   * browser events.
283
   */
284
  public init(options?: PointerInitOptions) {
285
    if (this.engine.isDisposed()) {
1,326!
286
      return;
×
287
    }
288
    // Disabling the touch action avoids browser/platform gestures from firing on the canvas
289
    // It is important on mobile to have touch action 'none'
290
    // https://stackoverflow.com/questions/48124372/pointermove-event-not-working-with-touch-why-not
291
    if (this.target === this.engine.canvas) {
1,326✔
292
      this.engine.canvas.style.touchAction = 'none';
1,298✔
293
    } else {
294
      document.body.style.touchAction = 'none';
28✔
295
    }
296
    // Preferred pointer events
297
    if (window.PointerEvent) {
1,326!
298
      this.target.addEventListener('pointerdown', this._boundHandle);
1,326✔
299
      this.target.addEventListener('pointerup', this._boundHandle);
1,326✔
300
      this.target.addEventListener('pointermove', this._boundHandle);
1,326✔
301
      this.target.addEventListener('pointercancel', this._boundHandle);
1,326✔
302
    } else {
303
      // Touch Events
304
      this.target.addEventListener('touchstart', this._boundHandle);
×
305
      this.target.addEventListener('touchend', this._boundHandle);
×
306
      this.target.addEventListener('touchmove', this._boundHandle);
×
307
      this.target.addEventListener('touchcancel', this._boundHandle);
×
308

309
      // Mouse Events
310
      this.target.addEventListener('mousedown', this._boundHandle);
×
311
      this.target.addEventListener('mouseup', this._boundHandle);
×
312
      this.target.addEventListener('mousemove', this._boundHandle);
×
313
    }
314

315
    // MDN MouseWheelEvent
316
    const wheelOptions = {
1,326✔
317
      passive: !(
318
        this.engine.pageScrollPreventionMode === ScrollPreventionMode.All ||
2,652✔
319
        this.engine.pageScrollPreventionMode === ScrollPreventionMode.Canvas
320
      )
321
    };
322
    if ('onwheel' in document.createElement('div')) {
1,326!
323
      // Modern Browsers
324
      this.target.addEventListener('wheel', this._boundWheel, wheelOptions);
1,326✔
325
    } else if (document.onmousewheel !== undefined) {
×
326
      // Webkit and IE
327
      this.target.addEventListener('mousewheel', this._boundWheel, wheelOptions);
×
328
    } else {
329
      // Remaining browser and older Firefox
330
      this.target.addEventListener('MozMousePixelScroll', this._boundWheel, wheelOptions);
×
331
    }
332

333
    const grabWindowFocus = options?.grabWindowFocus ?? true;
1,326✔
334
    // Handle cross origin iframe
335
    if (grabWindowFocus && isCrossOriginIframe()) {
1,326!
336
      const grabFocus = () => {
×
337
        window.focus();
×
338
      };
339
      // Preferred pointer events
340
      if (window.PointerEvent) {
×
341
        this.target.addEventListener('pointerdown', grabFocus);
×
342
      } else {
343
        // Touch Events
344
        this.target.addEventListener('touchstart', grabFocus);
×
345

346
        // Mouse Events
347
        this.target.addEventListener('mousedown', grabFocus);
×
348
      }
349
    }
350
  }
351

352
  public detach() {
353
    // Preferred pointer events
354
    if (window.PointerEvent) {
1!
355
      this.target.removeEventListener('pointerdown', this._boundHandle);
1✔
356
      this.target.removeEventListener('pointerup', this._boundHandle);
1✔
357
      this.target.removeEventListener('pointermove', this._boundHandle);
1✔
358
      this.target.removeEventListener('pointercancel', this._boundHandle);
1✔
359
    } else {
360
      // Touch Events
361
      this.target.removeEventListener('touchstart', this._boundHandle);
×
362
      this.target.removeEventListener('touchend', this._boundHandle);
×
363
      this.target.removeEventListener('touchmove', this._boundHandle);
×
364
      this.target.removeEventListener('touchcancel', this._boundHandle);
×
365

366
      // Mouse Events
367
      this.target.removeEventListener('mousedown', this._boundHandle);
×
368
      this.target.removeEventListener('mouseup', this._boundHandle);
×
369
      this.target.removeEventListener('mousemove', this._boundHandle);
×
370
    }
371

372
    if ('onwheel' in document.createElement('div')) {
1!
373
      // Modern Browsers
374
      this.target.removeEventListener('wheel', this._boundWheel);
1✔
375
    } else if (document.onmousewheel !== undefined) {
×
376
      // Webkit and IE
377
      this.target.addEventListener('mousewheel', this._boundWheel);
×
378
    } else {
379
      // Remaining browser and older Firefox
380
      this.target.addEventListener('MozMousePixelScroll', this._boundWheel);
×
381
    }
382
  }
383

384
  /**
385
   * Take native pointer id and map it to index in active pointers
386
   * @param nativePointerId
387
   */
388
  private _normalizePointerId(nativePointerId: number) {
389
    // Add to the the native pointer set id
390
    this._activeNativePointerIdsToNormalized.set(nativePointerId, -1);
329✔
391

392
    // Native pointer ids in ascending order
393
    const currentPointerIds = Array.from(this._activeNativePointerIdsToNormalized.keys()).sort((a, b) => a - b);
329✔
394

395
    // The index into sorted ids will be the new id, will always have an id
396
    const id = currentPointerIds.findIndex((p) => p === nativePointerId);
329✔
397

398
    // Save the mapping so we can reverse it later
399
    this._activeNativePointerIdsToNormalized.set(nativePointerId, id);
329✔
400

401
    // ignore pointer because game isn't watching
402
    return id;
329✔
403
  }
404

405
  /**
406
   * Responsible for handling and parsing pointer events
407
   */
408
  private _handle(ev: NativeTouchEvent | NativePointerEvent | NativeMouseEvent) {
409
    if (!this._enabled) {
576✔
410
      return;
247✔
411
    }
412
    ev.preventDefault();
329✔
413
    const eventCoords = new Map<number, GlobalCoordinates>();
329✔
414
    let button: PointerButton;
415
    let pointerType: PointerType;
416
    if (isTouchEvent(ev)) {
329!
417
      button = PointerButton.Unknown;
×
418
      pointerType = PointerType.Touch;
×
419
      // https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent
420
      for (let i = 0; i < ev.changedTouches.length; i++) {
×
421
        const touch = ev.changedTouches[i];
×
422
        const coordinates = GlobalCoordinates.fromPagePosition(touch.pageX, touch.pageY, this.engine);
×
423
        const nativePointerId = i + 1;
×
424
        const pointerId = this._normalizePointerId(nativePointerId);
×
425
        this.currentFramePointerCoords.set(pointerId, coordinates);
×
426
        eventCoords.set(pointerId, coordinates);
×
427
      }
428
    } else {
429
      button = this._nativeButtonToPointerButton(ev.button);
329✔
430
      pointerType = PointerType.Mouse;
329✔
431
      const coordinates = GlobalCoordinates.fromPagePosition(ev.pageX, ev.pageY, this.engine);
329✔
432
      let nativePointerId = 1;
329✔
433
      if (isPointerEvent(ev)) {
329!
434
        nativePointerId = ev.pointerId;
329✔
435
        pointerType = this._stringToPointerType(ev.pointerType);
329✔
436
      }
437
      const pointerId = this._normalizePointerId(nativePointerId);
329✔
438
      this.currentFramePointerCoords.set(pointerId, coordinates);
329✔
439
      eventCoords.set(pointerId, coordinates);
329✔
440
    }
441

442
    for (const [pointerId, coord] of eventCoords.entries()) {
329✔
443
      switch (ev.type) {
329✔
444
        case 'mousedown':
656!
445
        case 'pointerdown':
446
        case 'touchstart':
447
          this.currentFrameDown.push(new PointerEvent('down', pointerId, button, pointerType, coord, ev));
234✔
448
          this.currentFramePointerDown.set(pointerId, true);
234✔
449
          break;
234✔
450
        case 'mouseup':
451
        case 'pointerup':
452
        case 'touchend':
453
          this.currentFrameUp.push(new PointerEvent('up', pointerId, button, pointerType, coord, ev));
44✔
454
          this.currentFramePointerDown.set(pointerId, false);
44✔
455
          break;
44✔
456
        case 'mousemove':
457
        case 'pointermove':
458
        case 'touchmove':
459
          this.currentFrameMove.push(new PointerEvent('move', pointerId, button, pointerType, coord, ev));
49✔
460
          break;
49✔
461
        case 'touchcancel':
462
        case 'pointercancel':
463
          this.currentFrameCancel.push(new PointerEvent('cancel', pointerId, button, pointerType, coord, ev));
2✔
464
          break;
2✔
465
      }
466
    }
467
  }
468

469
  private _handleWheel(ev: NativeWheelEvent) {
470
    if (!this._enabled) {
16✔
471
      return;
6✔
472
    }
473
    // Should we prevent page scroll because of this event
474
    if (
10!
475
      this.engine.pageScrollPreventionMode === ScrollPreventionMode.All ||
30✔
476
      (this.engine.pageScrollPreventionMode === ScrollPreventionMode.Canvas && ev.target === this.engine.canvas)
477
    ) {
478
      ev.preventDefault();
×
479
    }
480
    const screen = this.engine.screen.pageToScreenCoordinates(vec(ev.pageX, ev.pageY));
10✔
481
    const world = this.engine.screen.screenToWorldCoordinates(screen);
10✔
482

483
    /**
484
     * A constant used to normalize wheel events across different browsers
485
     *
486
     * This normalization factor is pulled from
487
     * https://developer.mozilla.org/en-US/docs/Web/Events/wheel#Listening_to_this_event_across_browser
488
     */
489
    const ScrollWheelNormalizationFactor = -1 / 40;
10✔
490

491
    const deltaX = ev.deltaX || ev.wheelDeltaX * ScrollWheelNormalizationFactor || 0;
10✔
492
    const deltaY =
493
      ev.deltaY || ev.wheelDeltaY * ScrollWheelNormalizationFactor || ev.wheelDelta * ScrollWheelNormalizationFactor || ev.detail || 0;
10✔
494
    const deltaZ = ev.deltaZ || 0;
10✔
495
    let deltaMode = WheelDeltaMode.Pixel;
10✔
496

497
    if (ev.deltaMode) {
10!
498
      if (ev.deltaMode === 1) {
×
499
        deltaMode = WheelDeltaMode.Line;
×
500
      } else if (ev.deltaMode === 2) {
×
501
        deltaMode = WheelDeltaMode.Page;
×
502
      }
503
    }
504

505
    const we = new WheelEvent(world.x, world.y, ev.pageX, ev.pageY, screen.x, screen.y, 0, deltaX, deltaY, deltaZ, deltaMode, ev);
10✔
506
    this.currentFrameWheel.push(we);
10✔
507
  }
508

509
  /**
510
   * Triggers an excalibur pointer event in a world space pos
511
   *
512
   * Useful for testing pointers in excalibur
513
   * @param type
514
   * @param pos
515
   */
516
  public triggerEvent(type: 'down' | 'up' | 'move' | 'cancel', pos: Vector) {
517
    const page = this.engine.screen.worldToPageCoordinates(pos);
26✔
518
    // Send an event to the event receiver
519
    if (window.PointerEvent) {
26!
520
      this._handle(
26✔
521
        new window.PointerEvent('pointer' + type, {
522
          pointerId: 0,
523
          clientX: page.x,
524
          clientY: page.y
525
        })
526
      );
527
    } else {
528
      // Safari hack
529
      this._handle(
×
530
        new window.MouseEvent('mouse' + type, {
531
          clientX: page.x,
532
          clientY: page.y
533
        })
534
      );
535
    }
536

537
    // Force update pointer system
538
    const pointerSystem = this.engine.currentScene.world.get(PointerSystem);
26✔
539
    pointerSystem.preupdate(this.engine.currentScene, 1);
26✔
540
    pointerSystem.update(1);
26✔
541
  }
542

543
  private _nativeButtonToPointerButton(s: NativePointerButton): PointerButton {
544
    switch (s) {
329✔
545
      case NativePointerButton.NoButton:
329!
546
        return PointerButton.NoButton;
×
547
      case NativePointerButton.Left:
548
        return PointerButton.Left;
285✔
549
      case NativePointerButton.Middle:
550
        return PointerButton.Middle;
22✔
551
      case NativePointerButton.Right:
552
        return PointerButton.Right;
22✔
553
      case NativePointerButton.Unknown:
554
        return PointerButton.Unknown;
×
555
      default:
556
        return fail(s);
×
557
    }
558
  }
559

560
  private _stringToPointerType(s: string) {
561
    switch (s) {
329✔
562
      case 'touch':
329!
563
        return PointerType.Touch;
×
564
      case 'mouse':
565
        return PointerType.Mouse;
×
566
      case 'pen':
567
        return PointerType.Pen;
×
568
      default:
569
        return PointerType.Unknown;
329✔
570
    }
571
  }
572
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc