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

excaliburjs / Excalibur / 7294397098

21 Dec 2023 11:56PM UTC coverage: 91.859% (-0.06%) from 91.915%
7294397098

push

github

web-flow
fix: #2848 Sprite tint respected in constructor (#2852)

Closes #2848 

## Changes:

- Fixes sprite tint constructor value passing
- Fixes sprite tint on cloned sprites
- Fixes width/height inconsistency discovered in new tests
- New tests

4602 of 5758 branches covered (0.0%)

7 of 7 new or added lines in 3 files covered. (100.0%)

7 existing lines in 2 files now uncovered.

10686 of 11633 relevant lines covered (91.86%)

27166.71 hits per line

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

70.31
/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

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

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

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

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

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

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

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

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

68
  public currentFramePointerDown = new Map<number, boolean>();
600✔
69
  public lastFramePointerDown = new Map<number, boolean>();
600✔
70

71
  public currentFrameDown: PointerEvent[] = [];
600✔
72
  public currentFrameUp: PointerEvent[] = [];
600✔
73
  public currentFrameMove: PointerEvent[] = [];
600✔
74
  public currentFrameCancel: PointerEvent[] = [];
600✔
75
  public currentFrameWheel: WheelEvent[] = [];
600✔
76

77
  constructor(public readonly target: GlobalEventHandlers & EventTarget, public engine: Engine) {}
600✔
78

79
  /**
80
   * Creates a new PointerEventReceiver with a new target and engine while preserving existing pointer event
81
   * handlers.
82
   * @param target
83
   * @param engine
84
   */
85
  public recreate(target: GlobalEventHandlers & EventTarget, engine: Engine) {
86
    const eventReceiver = new PointerEventReceiver(target, engine);
1✔
87
    eventReceiver.primary = this.primary;
1✔
88
    eventReceiver._pointers = this._pointers;
1✔
89
    return eventReceiver;
1✔
90
  }
91

92
  private _pointers: PointerAbstraction[] = [this.primary];
600✔
93
  /**
94
   * Locates a specific pointer by id, creates it if it doesn't exist
95
   * @param index
96
   */
97
  public at(index: number): PointerAbstraction {
98
    if (index >= this._pointers.length) {
43!
99
      // Ensure there is a pointer to retrieve
100
      for (let i = this._pointers.length - 1, max = index; i < max; i++) {
×
101
        this._pointers.push(new PointerAbstraction());
×
102
      }
103
    }
104
    return this._pointers[index];
43✔
105
  }
106

107
  /**
108
   * The number of pointers currently being tracked by excalibur
109
   */
110
  public count(): number {
111
    return this._pointers.length;
×
112
  }
113

114
  /**
115
   * Is the specified pointer id down this frame
116
   * @param pointerId
117
   */
118
  public isDown(pointerId: number) {
119
    return this.currentFramePointerDown.get(pointerId) ?? false;
42✔
120
  }
121

122
  /**
123
   * Was the specified pointer id down last frame
124
   * @param pointerId
125
   */
126
  public wasDown(pointerId: number) {
127
    return this.lastFramePointerDown.get(pointerId) ?? false;
16✔
128
  }
129

130
  /**
131
   * Whether the Pointer is currently dragging.
132
   */
133
  public isDragging(pointerId: number): boolean {
134
    return this.isDown(pointerId);
26✔
135
  }
136

137
  /**
138
   * Whether the Pointer just started dragging.
139
   */
140
  public isDragStart(pointerId: number): boolean {
141
    return this.isDown(pointerId) && !this.wasDown(pointerId);
13✔
142
  }
143

144
  /**
145
   * Whether the Pointer just ended dragging.
146
   */
147
  public isDragEnd(pointerId: number): boolean {
148
    return !this.isDown(pointerId) && this.wasDown(pointerId);
3✔
149
  }
150

151
  public emit<TEventName extends EventKey<PointerEvents>>(eventName: TEventName, event: PointerEvents[TEventName]): void;
152
  public emit(eventName: string, event?: any): void;
153
  public emit<TEventName extends EventKey<PointerEvents> | string>(eventName: TEventName, event?: any): void {
154
    this.events.emit(eventName, event);
35✔
155
  }
156

157
  public on<TEventName extends EventKey<PointerEvents>>(eventName: TEventName, handler: Handler<PointerEvents[TEventName]>): Subscription;
158
  public on(eventName: string, handler: Handler<unknown>): Subscription;
159
  public on<TEventName extends EventKey<PointerEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription {
160
    return this.events.on(eventName, handler);
4✔
161
  }
162

163
  public once<TEventName extends EventKey<PointerEvents>>(eventName: TEventName, handler: Handler<PointerEvents[TEventName]>): Subscription;
164
  public once(eventName: string, handler: Handler<unknown>): Subscription;
165
  public once<TEventName extends EventKey<PointerEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription {
166
    return this.events.once(eventName, handler);
×
167
  }
168

169
  public off<TEventName extends EventKey<PointerEvents>>(eventName: TEventName, handler: Handler<PointerEvents[TEventName]>): void;
170
  public off(eventName: string, handler: Handler<unknown>): void;
171
  public off(eventName: string): void;
172
  public off<TEventName extends EventKey<PointerEvents> | string>(eventName: TEventName, handler?: Handler<any>): void {
173
    this.events.off(eventName, handler);
×
174
  }
175

176
  /**
177
   * Called internally by excalibur
178
   *
179
   * Updates the current frame pointer info and emits raw pointer events
180
   *
181
   * This does not emit events to entities, see PointerSystem
182
   */
183
  public update() {
184
    this.lastFramePointerDown = new Map(this.currentFramePointerDown);
1,623✔
185
    this.lastFramePointerCoords = new Map(this.currentFramePointerCoords);
1,623✔
186

187
    for (const event of this.currentFrameDown) {
1,623✔
188
      this.emit('down', event);
15✔
189
      const pointer = this.at(event.pointerId);
15✔
190
      pointer.emit('down', event);
15✔
191
      this.primary.emit('pointerdown', event);
15✔
192
    }
193

194
    for (const event of this.currentFrameUp) {
1,623✔
195
      this.emit('up', event);
7✔
196
      const pointer = this.at(event.pointerId);
7✔
197
      pointer.emit('up', event);
7✔
198
    }
199

200
    for (const event of this.currentFrameMove) {
1,623✔
201
      this.emit('move', event);
12✔
202
      const pointer = this.at(event.pointerId);
12✔
203
      pointer.emit('move', event);
12✔
204
    }
205

206
    for (const event of this.currentFrameCancel) {
1,623✔
207
      this.emit('cancel', event);
×
208
      const pointer = this.at(event.pointerId);
×
209
      pointer.emit('cancel', event);
×
210
    }
211

212
    for (const event of this.currentFrameWheel) {
1,623✔
213
      this.emit('wheel', event);
1✔
214
      this.primary.emit('pointerwheel', event);
1✔
215
      this.primary.emit('wheel', event);
1✔
216
    }
217
  }
218

219
  /**
220
   * Clears the current frame event and pointer data
221
   */
222
  public clear() {
223
    for (const event of this.currentFrameUp) {
1,623✔
224
      this.currentFramePointerCoords.delete(event.pointerId);
7✔
225
      const ids = this._activeNativePointerIdsToNormalized.entries();
7✔
226
      for (const [native, normalized] of ids) {
7✔
227
        if (normalized === event.pointerId) {
5!
228
          this._activeNativePointerIdsToNormalized.delete(native);
5✔
229
        }
230
      }
231
    }
232
    this.currentFrameDown.length = 0;
1,623✔
233
    this.currentFrameUp.length = 0;
1,623✔
234
    this.currentFrameMove.length = 0;
1,623✔
235
    this.currentFrameCancel.length = 0;
1,623✔
236
    this.currentFrameWheel.length = 0;
1,623✔
237
  }
238

239
  private _boundHandle = this._handle.bind(this);
600✔
240
  private _boundWheel = this._handleWheel.bind(this);
600✔
241
  /**
242
   * Initializes the pointer event receiver so that it can start listening to native
243
   * browser events.
244
   */
245
  public init(options?: PointerInitOptions) {
246
    // Disabling the touch action avoids browser/platform gestures from firing on the canvas
247
    // It is important on mobile to have touch action 'none'
248
    // https://stackoverflow.com/questions/48124372/pointermove-event-not-working-with-touch-why-not
249
    if (this.target === this.engine.canvas) {
600✔
250
      this.engine.canvas.style.touchAction = 'none';
588✔
251
    } else {
252
      document.body.style.touchAction = 'none';
12✔
253
    }
254
    // Preferred pointer events
255
    if (window.PointerEvent) {
600!
256
      this.target.addEventListener('pointerdown', this._boundHandle);
600✔
257
      this.target.addEventListener('pointerup', this._boundHandle);
600✔
258
      this.target.addEventListener('pointermove', this._boundHandle);
600✔
259
      this.target.addEventListener('pointercancel', this._boundHandle);
600✔
260
    } else {
261
      // Touch Events
262
      this.target.addEventListener('touchstart', this._boundHandle);
×
263
      this.target.addEventListener('touchend', this._boundHandle);
×
264
      this.target.addEventListener('touchmove', this._boundHandle);
×
265
      this.target.addEventListener('touchcancel', this._boundHandle);
×
266

267
      // Mouse Events
268
      this.target.addEventListener('mousedown', this._boundHandle);
×
269
      this.target.addEventListener('mouseup', this._boundHandle);
×
270
      this.target.addEventListener('mousemove', this._boundHandle);
×
271
    }
272

273
    // MDN MouseWheelEvent
274
    const wheelOptions = {
600✔
275
      passive: !(
276
        this.engine.pageScrollPreventionMode === ScrollPreventionMode.All ||
1,200✔
277
        this.engine.pageScrollPreventionMode === ScrollPreventionMode.Canvas
278
      )
279
    };
280
    if ('onwheel' in document.createElement('div')) {
600!
281
      // Modern Browsers
282
      this.target.addEventListener('wheel', this._boundWheel, wheelOptions);
600✔
283
    } else if (document.onmousewheel !== undefined) {
×
284
      // Webkit and IE
285
      this.target.addEventListener('mousewheel', this._boundWheel, wheelOptions);
×
286
    } else {
287
      // Remaining browser and older Firefox
288
      this.target.addEventListener('MozMousePixelScroll', this._boundWheel, wheelOptions);
×
289
    }
290

291
    const grabWindowFocus = options?.grabWindowFocus ?? true;
600✔
292
    // Handle cross origin iframe
293
    if (grabWindowFocus && isCrossOriginIframe()) {
600!
294
      const grabFocus = () => {
×
295
        window.focus();
×
296
      };
297
      // Preferred pointer events
298
      if (window.PointerEvent) {
×
299
        this.target.addEventListener('pointerdown', grabFocus);
×
300
      } else {
301
        // Touch Events
302
        this.target.addEventListener('touchstart', grabFocus);
×
303

304
        // Mouse Events
305
        this.target.addEventListener('mousedown', grabFocus);
×
306
      }
307
    }
308
  }
309

310
  public detach() {
311
    // Preferred pointer events
312
    if (window.PointerEvent) {
1!
313
      this.target.removeEventListener('pointerdown', this._boundHandle);
1✔
314
      this.target.removeEventListener('pointerup', this._boundHandle);
1✔
315
      this.target.removeEventListener('pointermove', this._boundHandle);
1✔
316
      this.target.removeEventListener('pointercancel', this._boundHandle);
1✔
317
    } else {
318
      // Touch Events
319
      this.target.removeEventListener('touchstart', this._boundHandle);
×
320
      this.target.removeEventListener('touchend', this._boundHandle);
×
321
      this.target.removeEventListener('touchmove', this._boundHandle);
×
322
      this.target.removeEventListener('touchcancel', this._boundHandle);
×
323

324
      // Mouse Events
325
      this.target.removeEventListener('mousedown', this._boundHandle);
×
326
      this.target.removeEventListener('mouseup', this._boundHandle);
×
327
      this.target.removeEventListener('mousemove', this._boundHandle);
×
328
    }
329

330
    if ('onwheel' in document.createElement('div')) {
1!
331
      // Modern Browsers
332
      this.target.removeEventListener('wheel', this._boundWheel);
1✔
333
    } else if (document.onmousewheel !== undefined) {
×
334
      // Webkit and IE
335
      this.target.addEventListener('mousewheel', this._boundWheel);
×
336
    } else {
337
      // Remaining browser and older Firefox
338
      this.target.addEventListener('MozMousePixelScroll', this._boundWheel);
×
339
    }
340
  }
341

342
  /**
343
   * Take native pointer id and map it to index in active pointers
344
   * @param nativePointerId
345
   */
346
  private _normalizePointerId(nativePointerId: number) {
347
    // Add to the the native pointer set id
348
    this._activeNativePointerIdsToNormalized.set(nativePointerId, -1);
129✔
349

350
    // Native pointer ids in ascending order
351
    const currentPointerIds = Array.from(this._activeNativePointerIdsToNormalized.keys()).sort((a, b) => a - b);
129✔
352

353
    // The index into sorted ids will be the new id, will always have an id
354
    const id = currentPointerIds.findIndex(p => p === nativePointerId);
129✔
355

356
    // Save the mapping so we can reverse it later
357
    this._activeNativePointerIdsToNormalized.set(nativePointerId, id);
129✔
358

359
    // ignore pointer because game isn't watching
360
    return id;
129✔
361
  }
362

363
  /**
364
   * Responsible for handling and parsing pointer events
365
   */
366
  private _handle(ev: NativeTouchEvent | NativePointerEvent | NativeMouseEvent) {
367
    ev.preventDefault();
129✔
368
    const eventCoords = new Map<number, GlobalCoordinates>();
129✔
369
    let button: PointerButton;
370
    let pointerType: PointerType;
371
    if (isTouchEvent(ev)) {
129!
372
      button = PointerButton.Unknown;
×
373
      pointerType = PointerType.Touch;
×
374
      // https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent
375
      for (let i = 0; i < ev.changedTouches.length; i++) {
×
376
        const touch = ev.changedTouches[i];
×
377
        const coordinates = GlobalCoordinates.fromPagePosition(touch.pageX, touch.pageY, this.engine);
×
378
        const nativePointerId = i + 1;
×
379
        const pointerId = this._normalizePointerId(nativePointerId);
×
380
        this.currentFramePointerCoords.set(pointerId, coordinates);
×
381
        eventCoords.set(pointerId, coordinates);
×
382
      }
383
    } else {
384
      button = this._nativeButtonToPointerButton(ev.button);
129✔
385
      pointerType = PointerType.Mouse;
129✔
386
      const coordinates = GlobalCoordinates.fromPagePosition(ev.pageX, ev.pageY, this.engine);
129✔
387
      let nativePointerId = 1;
129✔
388
      if (isPointerEvent(ev)) {
129!
389
        nativePointerId = ev.pointerId;
129✔
390
        pointerType = this._stringToPointerType(ev.pointerType);
129✔
391
      }
392
      const pointerId = this._normalizePointerId(nativePointerId);
129✔
393
      this.currentFramePointerCoords.set(pointerId, coordinates);
129✔
394
      eventCoords.set(pointerId, coordinates);
129✔
395
    }
396

397
    for (const [pointerId, coord] of eventCoords.entries()) {
129✔
398
      switch (ev.type) {
129✔
399
        case 'mousedown':
258!
400
        case 'pointerdown':
401
        case 'touchstart':
402
          this.currentFrameDown.push(new PointerEvent('down', pointerId, button, pointerType, coord, ev));
43✔
403
          this.currentFramePointerDown.set(pointerId, true);
43✔
404
          break;
43✔
405
        case 'mouseup':
406
        case 'pointerup':
407
        case 'touchend':
408
          this.currentFrameUp.push(new PointerEvent('up', pointerId, button, pointerType, coord, ev));
36✔
409
          this.currentFramePointerDown.set(pointerId, false);
36✔
410
          break;
36✔
411
        case 'mousemove':
412
        case 'pointermove':
413
        case 'touchmove':
414
          this.currentFrameMove.push(new PointerEvent('move', pointerId, button, pointerType, coord, ev));
50✔
415
          break;
50✔
416
        case 'touchcancel':
417
        case 'pointercancel':
418
          this.currentFrameCancel.push(new PointerEvent('cancel', pointerId, button, pointerType, coord, ev));
×
419
          break;
×
420
      }
421
    }
422
  }
423

424
  private _handleWheel(ev: NativeWheelEvent) {
425
    // Should we prevent page scroll because of this event
426
    if (
7!
427
      this.engine.pageScrollPreventionMode === ScrollPreventionMode.All ||
21✔
428
      (this.engine.pageScrollPreventionMode === ScrollPreventionMode.Canvas && ev.target === this.engine.canvas)
429
    ) {
430
      ev.preventDefault();
×
431
    }
432
    const screen = this.engine.screen.pageToScreenCoordinates(vec(ev.pageX, ev.pageY));
7✔
433
    const world = this.engine.screen.screenToWorldCoordinates(screen);
7✔
434

435
    /**
436
     * A constant used to normalize wheel events across different browsers
437
     *
438
     * This normalization factor is pulled from
439
     * https://developer.mozilla.org/en-US/docs/Web/Events/wheel#Listening_to_this_event_across_browser
440
     */
441
    const ScrollWheelNormalizationFactor = -1 / 40;
7✔
442

443
    const deltaX = ev.deltaX || ev.wheelDeltaX * ScrollWheelNormalizationFactor || 0;
7✔
444
    const deltaY =
445
        ev.deltaY || ev.wheelDeltaY * ScrollWheelNormalizationFactor || ev.wheelDelta * ScrollWheelNormalizationFactor || ev.detail || 0;
7✔
446
    const deltaZ = ev.deltaZ || 0;
7✔
447
    let deltaMode = WheelDeltaMode.Pixel;
7✔
448

449
    if (ev.deltaMode) {
7!
450
      if (ev.deltaMode === 1) {
×
451
        deltaMode = WheelDeltaMode.Line;
×
452
      } else if (ev.deltaMode === 2) {
×
453
        deltaMode = WheelDeltaMode.Page;
×
454
      }
455
    }
456

457
    const we = new WheelEvent(world.x, world.y, ev.pageX, ev.pageY, screen.x, screen.y, 0, deltaX, deltaY, deltaZ, deltaMode, ev);
7✔
458
    this.currentFrameWheel.push(we);
7✔
459
  }
460

461
  /**
462
   * Triggers an excalibur pointer event in a world space pos
463
   *
464
   * Useful for testing pointers in excalibur
465
   * @param type
466
   * @param pos
467
   */
468
  public triggerEvent(type: 'down' | 'up' | 'move' | 'cancel', pos: Vector) {
469
    const page = this.engine.screen.worldToPageCoordinates(pos);
18✔
470
    // Send an event to the event receiver
471
    if (window.PointerEvent) {
18!
472
      this._handle(new window.PointerEvent('pointer' + type, {
18✔
473
        pointerId: 0,
474
        clientX: page.x,
475
        clientY: page.y
476
      }));
477
    } else {
478
      // Safari hack
479
      this._handle(new window.MouseEvent('mouse' + type, {
×
480
        clientX: page.x,
481
        clientY: page.y
482
      }));
483
    }
484

485
    // Force update pointer system
486
    const pointerSystem = this.engine.currentScene.world.systemManager.get(PointerSystem);
18✔
487
    const transformEntities = this.engine.currentScene.world.queryManager.createQuery(pointerSystem.types);
18✔
488
    pointerSystem.preupdate();
18✔
489
    pointerSystem.update(transformEntities.getEntities());
18✔
490
  }
491

492
  private _nativeButtonToPointerButton(s: NativePointerButton): PointerButton {
493
    switch (s) {
129✔
494
      case NativePointerButton.NoButton:
129!
495
        return PointerButton.NoButton;
×
496
      case NativePointerButton.Left:
497
        return PointerButton.Left;
109✔
498
      case NativePointerButton.Middle:
499
        return PointerButton.Middle;
10✔
500
      case NativePointerButton.Right:
501
        return PointerButton.Right;
10✔
502
      case NativePointerButton.Unknown:
503
        return PointerButton.Unknown;
×
504
      default:
505
        return fail(s);
×
506
    }
507
  }
508

509
  private _stringToPointerType(s: string) {
510
    switch (s) {
129✔
511
      case 'touch':
129!
512
        return PointerType.Touch;
×
513
      case 'mouse':
514
        return PointerType.Mouse;
×
515
      case 'pen':
516
        return PointerType.Pen;
×
517
      default:
518
        return PointerType.Unknown;
129✔
519
    }
520
  }
521
}
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

© 2025 Coveralls, Inc