[READ-ONLY] Mirror of https://github.com/excaliburjs/Excalibur. 🎮 Your friendly TypeScript 2D game engine for the web 🗡️ excaliburjs.com
excalibur excaliburjs game-development game-engine game-framework gamedev games html5-canvas typescript
2

Configure Feed

Select the types of activity you want to include in your feed.

refactor: Pointer System refactor (#2071)

This is still work in progress, the goal is for this to be nearly 100% backwards compatible
- [x] Re-implement missing API surface
- [x] Remove old pointer system
- [x] Fix tests


Related #1161 Pointer Refactor

## Changes:

- Refactors pointer system into ECS style system
- Pointer component allows for configuring
- Will add the ability to use the collider and or the graphics as the target for pointer events

authored by

Erik Onarheim and committed by
GitHub
(Nov 9, 2021, 10:15 PM -0600) bc7986bd 659ecc2d

+1175 -1615
+1
.eslintrc.json
··· 75 75 "jsdoc/require-returns": 0, 76 76 "jsdoc/require-returns-type": 0, 77 77 "jsdoc/newline-after-description": 0, 78 + "jsdoc/no-multi-asterisks": 0, 78 79 "jsdoc/check-tag-names": ["error", { "definedTags": ["hidden", "internal", "source", "obsolete", "warning", "notimplemented", "credit", "typedoc"]}] 79 80 } 80 81 }
+11 -1
CHANGELOG.md
··· 15 15 16 16 ### Added 17 17 18 + - Pointers can now be configured to use the collider or the graphics bounds as the target for pointers with the `ex.PointerComponent` 19 + - `useColliderShape` - (default true) uses the collider component geometry for pointer events 20 + - `useGraphicsBounds` - (default false) uses the graphics bounds for pointer events 18 21 - 19 22 20 23 ### Fixed ··· 27 30 28 31 ### Changed 29 32 33 + - Pointer Events: 34 + * Event types (up, down, move, etc) now all exist in 2 types `ex.Input.PointerEvent` and `ex.Input.WheelEvent` 35 + * The `stopPropagation()` method used to cancel further dispatches has been renamed to `cancel()` to match other events API. 36 + * Events no longer have a reference to the `pointer` but now have all of the same information that was availabe on the pointer `worldPos`, `screenPos`, `pagePos` 30 37 - 31 38 32 39 <!--------------------------------- DO NOT EDIT BELOW THIS LINE ---------------------------------> ··· 42 49 - Story to show a play / pause implementation. 43 50 - `ex.Animation` now support `totalDuration` that will calculate automatically each frame duration based on how many frames have. 44 51 - `ex.Animation` now supports `.reverse()` to reverse the direction of play in an animation, use the `ex.Animation.direction` to inspect if the animation is playing in the `ex.AnimationDirection.Forward` direction or the `ex.AnimationDirection.Backward` direction. 45 - 46 52 ### Changed 47 53 54 + - Pointer system refactored into 2 parts: 55 + * First is an ECS style system `ex.PointerSystem` that dispatches events to Entities/Actors 56 + * Second is an event receiver `ex.PointerEventReceiver` which is responsible for collecting the native browser events 57 + * The API is mostly backwards compatible breaking changes are listed in the breaking change section, event types have been simplified, and `stopPropagation()` and been renamed to `cancel()` 48 58 - Internal Actions implementation converted to ECS system and component, this is a backwards compatible change with v0.25.0 49 59 - `ex.ActionsSystem` and `ex.ActionsComponent` now wrap the existing `ex.ActionContext` 50 60 - Actions can be shared with all entities now!
+20 -2
sandbox/src/game.ts
··· 640 640 }); 641 641 642 642 player.on('pointerdown', (e?: ex.Input.PointerEvent) => { 643 - alert('Player clicked!'); 643 + // alert('Player clicked!'); 644 + }); 645 + player.on('pointerdown', () => { 646 + console.log('pointer down'); 647 + }); 648 + player.on('pointerup', () => { 649 + console.log('pointer up'); 650 + }); 651 + player.on('pointermove', () => { 652 + //console.log('pointer over'); 653 + }); 654 + player.on('pointerleave', () => { 655 + console.log('pointer exit'); 656 + }); 657 + player.on('pointerenter', () => { 658 + console.log('pointer enter'); 659 + }); 660 + player.on('pointerwheel', () => { 661 + console.log('pointer wheel'); 644 662 }); 645 663 646 664 var newScene = new ex.Scene(); ··· 820 838 821 839 game.add(trigger); 822 840 823 - game.input.pointers.primary.on('down', (evt?: ex.Input.PointerEvent) => { 841 + game.input.pointers.primary.on('down', (evt: ex.Input.PointerEvent) => { 824 842 var c = tileMap.getCellByPoint(evt.worldPos.x, evt.worldPos.y); 825 843 if (c) { 826 844 if (c.solid) {
+2 -2
sandbox/tests/boundingbox/bbtester.ts
··· 38 38 // Drag box around 39 39 block2.on('pointerdragmove', (pe: ex.Input.PointerEvent) => { 40 40 if (boxPointerDragging) { 41 - block2.pos = pe.pointer.lastWorldPos; 41 + block2.pos = pe.worldPos; 42 42 } 43 43 }); 44 44 45 45 // Drag box around 46 46 block2.on('pointerdragleave', (pe: ex.Input.PointerEvent) => { 47 47 if (boxPointerDragging) { 48 - block2.pos = pe.pointer.lastWorldPos; 48 + block2.pos = pe.worldPos; 49 49 } 50 50 }); 51 51
+4 -4
sandbox/tests/input/pointer.ts
··· 31 31 32 32 box2.on('pointerdown', (pe: ex.Input.PointerEvent) => { 33 33 console.log('box2 clicked'); 34 - pe.stopPropagation(); 34 + pe.cancel(); 35 35 if (box2.color.toString() === ex.Color.White.toString()) { 36 36 box2.color = ex.Color.Yellow; 37 37 } else { ··· 52 52 // Drag box around 53 53 box.on('pointerdragmove', (pe: ex.Input.PointerEvent) => { 54 54 if (boxPointerDragging) { 55 - box.pos = pe.pointer.lastWorldPos; 55 + box.pos = pe.worldPos; 56 56 } 57 57 }); 58 58 59 59 // Drag box around 60 60 box.on('pointerdragleave', (pe: ex.Input.PointerEvent) => { 61 61 if (boxPointerDragging) { 62 - box.pos = pe.pointer.lastWorldPos; 62 + box.pos = pe.worldPos; 63 63 } 64 64 }); 65 65 ··· 69 69 70 70 // Follow cursor 71 71 game.input.pointers.primary.on('move', (pe: ex.Input.PointerEvent) => { 72 - cursor.pos = pe.pointer.lastWorldPos; 72 + cursor.pos = pe.worldPos; 73 73 }); 74 74 75 75 // Button type
+1 -1
sandbox/tests/physics/physics.ts
··· 141 141 } 142 142 }); 143 143 144 - game.input.pointers.primary.on('down', (evt: ex.Input.PointerDownEvent) => { 144 + game.input.pointers.primary.on('down', (evt: ex.Input.PointerEvent) => { 145 145 spawnBlock(evt.worldPos.x, evt.worldPos.y); 146 146 }); 147 147
+30 -84
src/engine/Actor.ts
··· 20 20 EnterViewPortEvent, 21 21 ExitViewPortEvent 22 22 } from './Events'; 23 - import { PointerEvent, WheelEvent, PointerDragEvent, PointerEventName } from './Input/PointerEvents'; 24 23 import { Engine } from './Engine'; 25 24 import { Color } from './Color'; 26 25 import { Sprite } from './Drawing/Sprite'; ··· 51 50 import { watch } from './Util/Watch'; 52 51 import { Collider, CollisionGroup } from './Collision/Index'; 53 52 import { Circle } from './Graphics/Circle'; 54 - import { CapturePointerConfig } from './Input/CapturePointerConfig'; 53 + import { PointerEvent } from './Input/PointerEvent'; 54 + import { WheelEvent } from './Input/WheelEvent'; 55 + import { PointerComponent } from './Input/PointerComponent'; 55 56 import { ActionsComponent } from './Actions/ActionsComponent'; 56 57 57 58 /** ··· 195 196 */ 196 197 public get collider(): ColliderComponent { 197 198 return this.get(ColliderComponent); 199 + } 200 + 201 + /** 202 + * Access to the Actor's built in [[PointerComponent]] config 203 + */ 204 + public get pointer(): PointerComponent { 205 + return this.get(PointerComponent); 198 206 } 199 207 200 208 /** ··· 427 435 428 436 private _pointerDragMoveHandler = (pe: PointerEvent) => { 429 437 if (this._dragging) { 430 - this.pos = pe.pointer.lastWorldPos; 438 + this.pos = pe.worldPos; 431 439 } 432 440 }; 433 441 434 442 private _pointerDragLeaveHandler = (pe: PointerEvent) => { 435 443 if (this._dragging) { 436 - this.pos = pe.pointer.lastWorldPos; 444 + this.pos = pe.worldPos; 437 445 } 438 446 }; 439 447 ··· 461 469 462 470 /** 463 471 * Modify the current actor update pipeline. 472 + * @deprecated will be removed in v0.26.0 464 473 */ 465 474 public traits: Trait[] = []; 466 475 ··· 478 487 } 479 488 private _color: Color; 480 489 481 - /** 482 - * Whether or not to enable the [[Traits.CapturePointer]] trait that propagates 483 - * pointer events to this actor 484 - */ 485 - public enableCapturePointer: boolean = false; 486 - 487 - /** 488 - * Configuration for [[Traits.CapturePointer]] trait 489 - */ 490 - public capturePointer: CapturePointerConfig = { 491 - captureMoveEvents: false, 492 - captureDragEvents: false 493 - }; 494 - 495 490 // #endregion 496 491 497 492 /** ··· 533 528 this.rotation = rotation ?? 0; 534 529 this.scale = scale ?? vec(1, 1); 535 530 this.z = z ?? 0; 531 + 532 + this.addComponent(new PointerComponent); 536 533 537 534 this.addComponent(new GraphicsComponent()); 538 535 this.addComponent(new CanvasDrawComponent((ctx, delta) => this.draw(ctx, delta))); ··· 588 585 // TODO remove offscreen trait after legacy drawing removed 589 586 this.traits.push(new Traits.OffscreenCulling()); 590 587 } 591 - this.traits.push(new Traits.CapturePointer()); 592 588 593 589 } 594 590 ··· 618 614 619 615 // #region Events 620 616 621 - private _capturePointerEvents: PointerEventName[] = [ 622 - 'pointerup', 623 - 'pointerdown', 624 - 'pointermove', 625 - 'pointerenter', 626 - 'pointerleave', 627 - 'pointerdragstart', 628 - 'pointerdragend', 629 - 'pointerdragmove', 630 - 'pointerdragenter', 631 - 'pointerdragleave' 632 - ]; 633 - 634 - private _captureMoveEvents: PointerEventName[] = [ 635 - 'pointermove', 636 - 'pointerenter', 637 - 'pointerleave', 638 - 'pointerdragmove', 639 - 'pointerdragenter', 640 - 'pointerdragleave' 641 - ]; 642 - 643 - private _captureDragEvents: PointerEventName[] = [ 644 - 'pointerdragstart', 645 - 'pointerdragend', 646 - 'pointerdragmove', 647 - 'pointerdragenter', 648 - 'pointerdragleave' 649 - ]; 650 - 651 - private _checkForPointerOptIn(eventName: string) { 652 - if (eventName) { 653 - const normalized = <PointerEventName>eventName.toLowerCase(); 654 - 655 - if (this._capturePointerEvents.indexOf(normalized) !== -1) { 656 - this.enableCapturePointer = true; 657 - 658 - if (this._captureMoveEvents.indexOf(normalized) !== -1) { 659 - this.capturePointer.captureMoveEvents = true; 660 - } 661 - 662 - if (this._captureDragEvents.indexOf(normalized) !== -1) { 663 - this.capturePointer.captureDragEvents = true; 664 - } 665 - } 666 - } 667 - } 668 - 669 617 public on(eventName: Events.exittrigger, handler: (event: ExitTriggerEvent) => void): void; 670 618 public on(eventName: Events.entertrigger, handler: (event: EnterTriggerEvent) => void): void; 671 619 /** ··· 720 668 public on(eventName: Events.pointermove, handler: (event: PointerEvent) => void): void; 721 669 public on(eventName: Events.pointercancel, handler: (event: PointerEvent) => void): void; 722 670 public on(eventName: Events.pointerwheel, handler: (event: WheelEvent) => void): void; 723 - public on(eventName: Events.pointerdragstart, handler: (event: PointerDragEvent) => void): void; 724 - public on(eventName: Events.pointerdragend, handler: (event: PointerDragEvent) => void): void; 725 - public on(eventName: Events.pointerdragenter, handler: (event: PointerDragEvent) => void): void; 726 - public on(eventName: Events.pointerdragleave, handler: (event: PointerDragEvent) => void): void; 727 - public on(eventName: Events.pointerdragmove, handler: (event: PointerDragEvent) => void): void; 671 + public on(eventName: Events.pointerdragstart, handler: (event: PointerEvent) => void): void; 672 + public on(eventName: Events.pointerdragend, handler: (event: PointerEvent) => void): void; 673 + public on(eventName: Events.pointerdragenter, handler: (event: PointerEvent) => void): void; 674 + public on(eventName: Events.pointerdragleave, handler: (event: PointerEvent) => void): void; 675 + public on(eventName: Events.pointerdragmove, handler: (event: PointerEvent) => void): void; 728 676 public on(eventName: Events.enterviewport, handler: (event: EnterViewPortEvent) => void): void; 729 677 public on(eventName: Events.exitviewport, handler: (event: ExitViewPortEvent) => void): void; 730 678 public on(eventName: string, handler: (event: GameEvent<Actor>) => void): void; 731 679 public on(eventName: string, handler: (event: any) => void): void { 732 - this._checkForPointerOptIn(eventName); 733 680 super.on(eventName, handler); 734 681 } 735 682 ··· 787 734 public once(eventName: Events.pointermove, handler: (event: PointerEvent) => void): void; 788 735 public once(eventName: Events.pointercancel, handler: (event: PointerEvent) => void): void; 789 736 public once(eventName: Events.pointerwheel, handler: (event: WheelEvent) => void): void; 790 - public once(eventName: Events.pointerdragstart, handler: (event: PointerDragEvent) => void): void; 791 - public once(eventName: Events.pointerdragend, handler: (event: PointerDragEvent) => void): void; 792 - public once(eventName: Events.pointerdragenter, handler: (event: PointerDragEvent) => void): void; 793 - public once(eventName: Events.pointerdragleave, handler: (event: PointerDragEvent) => void): void; 794 - public once(eventName: Events.pointerdragmove, handler: (event: PointerDragEvent) => void): void; 737 + public once(eventName: Events.pointerdragstart, handler: (event: PointerEvent) => void): void; 738 + public once(eventName: Events.pointerdragend, handler: (event: PointerEvent) => void): void; 739 + public once(eventName: Events.pointerdragenter, handler: (event: PointerEvent) => void): void; 740 + public once(eventName: Events.pointerdragleave, handler: (event: PointerEvent) => void): void; 741 + public once(eventName: Events.pointerdragmove, handler: (event: PointerEvent) => void): void; 795 742 public once(eventName: Events.enterviewport, handler: (event: EnterViewPortEvent) => void): void; 796 743 public once(eventName: Events.exitviewport, handler: (event: ExitViewPortEvent) => void): void; 797 744 public once(eventName: string, handler: (event: GameEvent<Actor>) => void): void; 798 745 public once(eventName: string, handler: (event: any) => void): void { 799 - this._checkForPointerOptIn(eventName); 800 746 super.once(eventName, handler); 801 747 } 802 748 ··· 844 790 public off(eventName: Events.pointermove, handler?: (event: PointerEvent) => void): void; 845 791 public off(eventName: Events.pointercancel, handler?: (event: PointerEvent) => void): void; 846 792 public off(eventName: Events.pointerwheel, handler?: (event: WheelEvent) => void): void; 847 - public off(eventName: Events.pointerdragstart, handler?: (event: PointerDragEvent) => void): void; 848 - public off(eventName: Events.pointerdragend, handler?: (event: PointerDragEvent) => void): void; 849 - public off(eventName: Events.pointerdragenter, handler?: (event: PointerDragEvent) => void): void; 850 - public off(eventName: Events.pointerdragleave, handler?: (event: PointerDragEvent) => void): void; 851 - public off(eventName: Events.pointerdragmove, handler?: (event: PointerDragEvent) => void): void; 793 + public off(eventName: Events.pointerdragstart, handler?: (event: PointerEvent) => void): void; 794 + public off(eventName: Events.pointerdragend, handler?: (event: PointerEvent) => void): void; 795 + public off(eventName: Events.pointerdragenter, handler?: (event: PointerEvent) => void): void; 796 + public off(eventName: Events.pointerdragleave, handler?: (event: PointerEvent) => void): void; 797 + public off(eventName: Events.pointerdragmove, handler?: (event: PointerEvent) => void): void; 852 798 public off(eventName: Events.prekill, handler?: (event: PreKillEvent) => void): void; 853 799 public off(eventName: Events.postkill, handler?: (event: PostKillEvent) => void): void; 854 800 public off(eventName: Events.initialize, handler?: (event: Events.InitializeEvent<Actor>) => void): void;
+4 -5
src/engine/Class.ts
··· 1 - import { GameEvent } from './Events'; 2 1 import { EventDispatcher } from './EventDispatcher'; 3 2 import { Eventable } from './Interfaces/Evented'; 4 3 ··· 22 21 * @param eventName Name of the event to listen for 23 22 * @param handler Event handler for the thrown event 24 23 */ 25 - public on(eventName: string, handler: (event: GameEvent<any>) => void) { 24 + public on(eventName: string, handler: (event: any) => void) { 26 25 this.eventDispatcher.on(eventName, handler); 27 26 } 28 27 ··· 34 33 * @param eventName Name of the event to listen for 35 34 * @param handler Event handler for the thrown event 36 35 */ 37 - public off(eventName: string, handler?: (event: GameEvent<any>) => void) { 36 + public off(eventName: string, handler?: (event: any) => void) { 38 37 this.eventDispatcher.off(eventName, handler); 39 38 } 40 39 ··· 43 42 * @param eventName Name of the event to emit 44 43 * @param eventObject Data associated with this event 45 44 */ 46 - public emit(eventName: string, eventObject: GameEvent<any>) { 45 + public emit(eventName: string, eventObject: any) { 47 46 this.eventDispatcher.emit(eventName, eventObject); 48 47 } 49 48 ··· 53 52 * @param eventName The name of the event to subscribe to once 54 53 * @param handler The handler of the event that will be auto unsubscribed 55 54 */ 56 - public once(eventName: string, handler: (event: GameEvent<any>) => void) { 55 + public once(eventName: string, handler: (event: any) => void) { 57 56 this.eventDispatcher.once(eventName, handler); 58 57 } 59 58 }
+4 -4
src/engine/Engine.ts
··· 41 41 import { BrowserEvents } from './Util/Browser'; 42 42 import { obsolete } from './Util/Decorators'; 43 43 import { ExcaliburGraphicsContext, ExcaliburGraphicsContext2DCanvas, ExcaliburGraphicsContextWebGL } from './Graphics'; 44 + import { PointerEventReceiver } from './Input/PointerEventReceiver'; 44 45 45 46 /** 46 47 * Enum representing the different mousewheel event bubble prevention ··· 898 899 this.pageScrollPreventionMode = options.scrollPreventionMode; 899 900 900 901 // initialize inputs 902 + const pointerTarget = options && options.pointerScope === Input.PointerScope.Document ? document : this.canvas; 901 903 this.input = { 902 904 keyboard: new Input.Keyboard(), 903 - pointers: new Input.Pointers(this), 905 + pointers: new PointerEventReceiver(pointerTarget, this), 904 906 gamepads: new Input.Gamepads() 905 907 }; 906 908 this.input.keyboard.init(); 907 - this.input.pointers.init(options && options.pointerScope === Input.PointerScope.Document ? document : this.canvas); 909 + this.input.pointers.init(); 908 910 this.input.gamepads.init(); 909 911 910 912 // Issue #385 make use of the visibility api ··· 989 991 this._loader.update(this, delta); 990 992 // Update input listeners 991 993 this.input.keyboard.update(); 992 - this.input.pointers.update(); 993 994 this.input.gamepads.update(); 994 995 return; 995 996 } ··· 1010 1011 1011 1012 // Update input listeners 1012 1013 this.input.keyboard.update(); 1013 - this.input.pointers.update(); 1014 1014 this.input.gamepads.update(); 1015 1015 1016 1016 // Publish update event
+12
src/engine/Events/ExEvent.ts
··· 1 + 2 + export abstract class ExEvent<TName extends string> { 3 + abstract readonly type: TName; 4 + 5 + private _active = true; 6 + public get active() { 7 + return this._active; 8 + } 9 + public cancel() { 10 + this._active = false; 11 + } 12 + }
+45
src/engine/Events/PointerEvents.ts
··· 1 + import { GlobalCoordinates } from '..'; 2 + import { WheelDeltaMode } from '../Input/WheelDeltaMode'; 3 + import { ExEvent } from './ExEvent'; 4 + 5 + 6 + export abstract class PointerEvent extends ExEvent<'down' | 'up' | 'move' | 'cancel'> { 7 + constructor( 8 + public pointerId: number, 9 + public coordinatess: GlobalCoordinates, 10 + public nativeEvent: Event) { 11 + super(); 12 + } 13 + } 14 + export class PointerUp extends PointerEvent { 15 + public readonly type = 'up' 16 + } 17 + export class PointerDown extends PointerEvent { 18 + public readonly type = 'down' 19 + } 20 + export class PointerMove extends PointerEvent { 21 + public readonly type = 'move' 22 + } 23 + export class PointerCancel extends PointerEvent { 24 + public readonly type = 'cancel' 25 + } 26 + 27 + export class Wheel extends ExEvent<'wheel'> { 28 + public readonly type = 'wheel' 29 + constructor( 30 + public x: number, 31 + public y: number, 32 + public pageX: number, 33 + public pageY: number, 34 + public screenX: number, 35 + public screenY: number, 36 + public index: number, 37 + public deltaX: number, 38 + public deltaY: number, 39 + public deltaZ: number, 40 + public deltaMode: WheelDeltaMode, 41 + public ev: Event 42 + ) { 43 + super(); 44 + } 45 + }
+2 -2
src/engine/Input/EngineInput.ts
··· 1 1 import { Keyboard } from './Keyboard'; 2 2 import { Gamepads } from './Gamepad'; 3 - import { Pointers } from './Pointers'; 3 + import { PointerEventReceiver } from './PointerEventReceiver'; 4 4 5 5 export interface EngineInput { 6 6 keyboard: Keyboard; 7 - pointers: Pointers; 7 + pointers: PointerEventReceiver; 8 8 gamepads: Gamepads; 9 9 }
+11 -3
src/engine/Input/Index.ts
··· 7 7 * @typedoc 8 8 */ 9 9 export * from './Gamepad'; 10 - export * from './Pointer'; 11 - export * from './Pointers'; 12 - export * from './PointerEvents'; 10 + export * from './PointerScope'; 11 + export * from './PointerType'; 12 + export * from './PointerSystem'; 13 + export * from './PointerComponent'; 14 + export * from './PointerEventReceiver'; 13 15 export * from './Keyboard'; 14 16 export * from './EngineInput'; 15 17 export * from './CapturePointerConfig'; 18 + 19 + export * from './NativePointerButton'; 20 + export * from './PointerButton'; 21 + export * from './WheelDeltaMode'; 22 + export * from './PointerEvent'; 23 + export * from './WheelEvent';
+10
src/engine/Input/NativePointerButton.ts
··· 1 + /** 2 + * Native browser button enumeration 3 + */ 4 + export enum NativePointerButton { 5 + NoButton = -1, 6 + Left = 0, 7 + Middle = 1, 8 + Right = 2, 9 + Unknown = 3 10 + }
-253
src/engine/Input/Pointer.ts
··· 1 - import { Actor } from '../Actor'; 2 - import { Vector } from '../Math/vector'; 3 - import { Class } from '../Class'; 4 - import * as Actors from '../Util/Actors'; 5 - import { removeItemFromArray } from '../Util/Util'; 6 - import { PointerMoveEvent, PointerDownEvent, PointerUpEvent, WheelEvent } from './PointerEvents'; 7 - 8 - export interface ActorsUnderPointer { 9 - [ActorId: number]: Actor; 10 - length: number; 11 - } 12 - 13 - /** 14 - * The type of pointer for a [[PointerEvent]]. 15 - */ 16 - export enum PointerType { 17 - Touch = 'Touch', 18 - Mouse = 'Mouse', 19 - Pen = 'Pen', 20 - Unknown = 'Unknown' 21 - } 22 - 23 - /** 24 - * Determines the scope of handling mouse/touch events. See [[Pointers]] for more information. 25 - */ 26 - export enum PointerScope { 27 - /** 28 - * Handle events on the `canvas` element only. Events originating outside the 29 - * `canvas` will not be handled. 30 - */ 31 - Canvas = 'Canvas', 32 - 33 - /** 34 - * Handles events on the entire document. All events will be handled by Excalibur. 35 - */ 36 - Document = 'Document' 37 - } 38 - 39 - /** 40 - * Captures and dispatches PointerEvents 41 - */ 42 - export class Pointer extends Class { 43 - private static _MAX_ID = 0; 44 - public readonly id = Pointer._MAX_ID++; 45 - private _isDown: boolean = false; 46 - private _wasDown: boolean = false; 47 - 48 - private _actorsUnderPointer: ActorsUnderPointer = { length: 0 }; 49 - 50 - private _actors: Actor[] = []; 51 - private _actorsLastFrame: Actor[] = []; 52 - private _actorsNoLongerUnderPointer: Actor[] = []; 53 - 54 - private _actorSortingFcn = (a: Actor, b: Actor) => { 55 - if (a.z === b.z) { 56 - return b.id - a.id; 57 - } 58 - return b.z - a.z; 59 - }; 60 - 61 - /** 62 - * Whether the Pointer is currently dragging. 63 - */ 64 - public get isDragging(): boolean { 65 - return this._isDown; 66 - } 67 - 68 - /** 69 - * Whether the Pointer just started dragging. 70 - */ 71 - public get isDragStart(): boolean { 72 - return !this._wasDown && this._isDown; 73 - } 74 - 75 - /** 76 - * Whether the Pointer just ended dragging. 77 - */ 78 - public get isDragEnd(): boolean { 79 - return this._wasDown && !this._isDown; 80 - } 81 - 82 - /** 83 - * Returns true if pointer has any actors under 84 - */ 85 - public get hasActorsUnderPointer(): boolean { 86 - return !!this._actorsUnderPointer.length; 87 - } 88 - 89 - /** 90 - * The last position on the document this pointer was at. Can be `null` if pointer was never active. 91 - */ 92 - public lastPagePos: Vector = null; 93 - 94 - /** 95 - * The last position on the screen this pointer was at. Can be `null` if pointer was never active. 96 - */ 97 - public lastScreenPos: Vector = null; 98 - 99 - /** 100 - * The last position in the game world coordinates this pointer was at. Can be `null` if pointer was never active. 101 - */ 102 - public lastWorldPos: Vector = null; 103 - 104 - /** 105 - * Returns the currently dragging target or null if it isn't exist 106 - */ 107 - public dragTarget: Actor = null; 108 - 109 - constructor() { 110 - super(); 111 - 112 - this.on('move', this._onPointerMove); 113 - this.on('down', this._onPointerDown); 114 - this.on('up', this._onPointerUp); 115 - } 116 - 117 - on(event: 'move', handler: (event: PointerMoveEvent) => void): void; 118 - on(event: 'down', handler: (event: PointerDownEvent) => void): void; 119 - on(event: 'up', handler: (event: PointerUpEvent) => void): void; 120 - on(event: 'wheel', handler: (event: WheelEvent) => void): void; 121 - on(event: string, handler: (event: any) => void): void { 122 - super.on(event, handler); 123 - } 124 - 125 - once(event: 'move', handler: (event: PointerMoveEvent) => void): void; 126 - once(event: 'down', handler: (event: PointerDownEvent) => void): void; 127 - once(event: 'up', handler: (event: PointerUpEvent) => void): void; 128 - once(event: 'wheel', handler: (event: WheelEvent) => void): void; 129 - once(event: string, handler: (event: any) => void): void { 130 - super.once(event, handler); 131 - } 132 - 133 - off(event: 'move', handler?: (event: PointerMoveEvent) => void): void; 134 - off(event: 'down', handler?: (event: PointerDownEvent) => void): void; 135 - off(event: 'up', handler?: (event: PointerUpEvent) => void): void; 136 - off(event: 'wheel', handler?: (event: WheelEvent) => void): void; 137 - off(event: string, handler?: (event: any) => void): void { 138 - super.off(event, handler); 139 - } 140 - 141 - /** 142 - * Update the state of current pointer, meant to be called a the end of frame 143 - */ 144 - public update(): void { 145 - if (this._wasDown && !this._isDown) { 146 - this._wasDown = false; 147 - } else if (!this._wasDown && this._isDown) { 148 - this._wasDown = true; 149 - } 150 - this._actorsLastFrame = [...this._actors]; 151 - this._actorsNoLongerUnderPointer = []; 152 - } 153 - 154 - /** 155 - * Adds an Actor to actorsUnderPointer object. 156 - * @param actor An Actor to be added; 157 - */ 158 - public addActorUnderPointer(actor: Actor): void { 159 - if (!this.isActorAliveUnderPointer(actor)) { 160 - this._actorsUnderPointer[actor.id] = actor; 161 - this._actorsUnderPointer.length += 1; 162 - this._actors.push(actor); 163 - } 164 - 165 - // Actors are processed in z-order highest z to lowest 166 - // ties are broken by id highest id (newest) to lowest id (oldest) 167 - this._actors.sort(this._actorSortingFcn); 168 - } 169 - 170 - /** 171 - * Removes an Actor from actorsUnderPointer object. 172 - * @param actor An Actor to be removed; 173 - */ 174 - public removeActorUnderPointer(actor: Actor): void { 175 - if (this.isActorAliveUnderPointer(actor)) { 176 - delete this._actorsUnderPointer[actor.id]; 177 - this._actorsUnderPointer.length -= 1; 178 - removeItemFromArray(actor, this._actors); 179 - this._actorsNoLongerUnderPointer.push(actor); 180 - } 181 - } 182 - 183 - /** 184 - * Returns all actors under this pointer this frame 185 - */ 186 - public getActorsUnderPointer(): Actor[] { 187 - return this._actors; 188 - } 189 - 190 - /** 191 - * Returns all actors that are no longer under the pointer this frame 192 - */ 193 - public getActorsUnderPointerLastFrame(): Actor[] { 194 - return this._actorsLastFrame; 195 - } 196 - 197 - /** 198 - * Returns all actors relevant for events to pointer this frame 199 - */ 200 - public getActorsForEvents(): Actor[] { 201 - return this._actors 202 - .concat(this._actorsLastFrame) 203 - .filter((actor, i, self) => { 204 - return self.indexOf(actor) === i; // de-dup 205 - }) 206 - .sort(this._actorSortingFcn); // sort by z 207 - } 208 - 209 - /** 210 - * Checks if Pointer location has a specific Actor bounds contained underneath. 211 - * @param actor An Actor for check; 212 - */ 213 - public checkActorUnderPointer(actor: Actor): boolean { 214 - if (this.lastWorldPos) { 215 - return actor.contains(this.lastWorldPos.x, this.lastWorldPos.y, !Actors.isScreenElement(actor)); 216 - } 217 - return false; 218 - } 219 - 220 - /** 221 - * Checks if an actor was under the pointer last frame 222 - * @param actor 223 - */ 224 - public wasActorUnderPointer(actor: Actor): boolean { 225 - return this._actorsLastFrame.indexOf(actor) > -1; 226 - } 227 - 228 - /** 229 - * Checks if Pointer has a specific Actor in ActorsUnderPointer list. 230 - * @param actor An Actor for check; 231 - */ 232 - public isActorAliveUnderPointer(actor: Actor): boolean { 233 - return !!(!actor.isKilled() && actor.scene && this._actorsUnderPointer.hasOwnProperty(actor.id.toString())); 234 - } 235 - 236 - private _onPointerMove(ev: PointerMoveEvent): void { 237 - this.lastPagePos = new Vector(ev.pagePos.x, ev.pagePos.y); 238 - this.lastScreenPos = new Vector(ev.screenPos.x, ev.screenPos.y); 239 - this.lastWorldPos = new Vector(ev.worldPos.x, ev.worldPos.y); 240 - } 241 - 242 - private _onPointerDown(ev: PointerDownEvent): void { 243 - this.lastPagePos = new Vector(ev.pagePos.x, ev.pagePos.y); 244 - this.lastScreenPos = new Vector(ev.screenPos.x, ev.screenPos.y); 245 - this.lastWorldPos = new Vector(ev.worldPos.x, ev.worldPos.y); 246 - this._isDown = true; 247 - } 248 - 249 - private _onPointerUp(_ev: PointerUpEvent): void { 250 - this._isDown = false; 251 - this.dragTarget = null; 252 - } 253 - }
+65
src/engine/Input/PointerAbstraction.ts
··· 1 + import { Class } from '../Class'; 2 + import { Vector } from '../Math/vector'; 3 + import { WheelEvent } from './WheelEvent'; 4 + import { PointerEvent } from './PointerEvent'; 5 + 6 + export class PointerAbstraction extends Class { 7 + 8 + /** 9 + * The last position on the document this pointer was at. Can be `null` if pointer was never active. 10 + */ 11 + public lastPagePos: Vector = Vector.Zero; 12 + 13 + /** 14 + * The last position on the screen this pointer was at. Can be `null` if pointer was never active. 15 + */ 16 + public lastScreenPos: Vector = Vector.Zero; 17 + 18 + /** 19 + * The last position in the game world coordinates this pointer was at. Can be `null` if pointer was never active. 20 + */ 21 + public lastWorldPos: Vector = Vector.Zero; 22 + 23 + constructor() { 24 + super(); 25 + this.on('move', this._onPointerMove); 26 + this.on('down', this._onPointerDown); 27 + } 28 + 29 + on(event: 'move', handler: (event: PointerEvent) => void): void; 30 + on(event: 'down', handler: (event: PointerEvent) => void): void; 31 + on(event: 'up', handler: (event: PointerEvent) => void): void; 32 + on(event: 'wheel', handler: (event: WheelEvent) => void): void; 33 + on(event: string, handler: (event: any) => void): void { 34 + super.on(event, handler); 35 + } 36 + 37 + once(event: 'move', handler: (event: PointerEvent) => void): void; 38 + once(event: 'down', handler: (event: PointerEvent) => void): void; 39 + once(event: 'up', handler: (event: PointerEvent) => void): void; 40 + once(event: 'wheel', handler: (event: WheelEvent) => void): void; 41 + once(event: string, handler: (event: any) => void): void { 42 + super.once(event, handler); 43 + } 44 + 45 + off(event: 'move', handler?: (event: PointerEvent) => void): void; 46 + off(event: 'down', handler?: (event: PointerEvent) => void): void; 47 + off(event: 'up', handler?: (event: PointerEvent) => void): void; 48 + off(event: 'wheel', handler?: (event: WheelEvent) => void): void; 49 + off(event: string, handler?: (event: any) => void): void { 50 + super.off(event, handler); 51 + } 52 + 53 + 54 + private _onPointerMove(ev: PointerEvent): void { 55 + this.lastPagePos = new Vector(ev.pagePos.x, ev.pagePos.y); 56 + this.lastScreenPos = new Vector(ev.screenPos.x, ev.screenPos.y); 57 + this.lastWorldPos = new Vector(ev.worldPos.x, ev.worldPos.y); 58 + } 59 + 60 + private _onPointerDown(ev: PointerEvent): void { 61 + this.lastPagePos = new Vector(ev.pagePos.x, ev.pagePos.y); 62 + this.lastScreenPos = new Vector(ev.screenPos.x, ev.screenPos.y); 63 + this.lastWorldPos = new Vector(ev.worldPos.x, ev.worldPos.y); 64 + } 65 + }
+10
src/engine/Input/PointerButton.ts
··· 1 + /** 2 + * The mouse button being pressed. 3 + */ 4 + export enum PointerButton { 5 + Left = 'Left', 6 + Middle = 'Middle', 7 + Right = 'Right', 8 + Unknown = 'Unknown', 9 + NoButton = 'NoButton' 10 + }
+25
src/engine/Input/PointerComponent.ts
··· 1 + import { Component } from '../EntityComponentSystem/Component'; 2 + 3 + /** 4 + * Add this component to optionally configure how the pointer 5 + * system detects pointer events. 6 + * 7 + * By default the collider shape is used and graphics bounds is not. 8 + * 9 + * If both collider shape and graphics bounds are enabled it will fire events if either or 10 + * are intersecting the pointer. 11 + */ 12 + export class PointerComponent extends Component<'ex.pointer'> { 13 + public readonly type = 'ex.pointer'; 14 + /** 15 + * Use any existsing Collider component geometry for pointer events. This is useful if you want 16 + * user pointer events only to trigger on the same collision geometry used in the collider component 17 + * for collision resolution. Default is `true`. 18 + */ 19 + public useColliderShape = true; 20 + /** 21 + * Use any existing Graphics component bounds for pointers. This is useful if you want the axis aligned 22 + * bounds around the graphic to trigger pointer events. Default is `false`. 23 + */ 24 + public useGraphicsBounds = false; 25 + }
+31
src/engine/Input/PointerEvent.ts
··· 1 + import { GlobalCoordinates } from '../Math/global-coordinates'; 2 + import { Vector } from '../Math/vector'; 3 + import { PointerButton } from './PointerButton'; 4 + import { PointerType } from './PointerType'; 5 + 6 + export class PointerEvent { 7 + public active = true; 8 + public cancel() { 9 + this.active = false; 10 + } 11 + 12 + get pagePos(): Vector { 13 + return this.coordinates.pagePos; 14 + } 15 + 16 + get screenPos(): Vector { 17 + return this.coordinates.screenPos; 18 + } 19 + 20 + get worldPos(): Vector { 21 + return this.coordinates.worldPos; 22 + } 23 + 24 + constructor( 25 + public type: 'down' | 'up' | 'move' | 'cancel', 26 + public pointerId: number, 27 + public button: PointerButton, 28 + public pointerType: PointerType, 29 + public coordinates: GlobalCoordinates, 30 + public nativeEvent: Event) { }; 31 + }
+440
src/engine/Input/PointerEventReceiver.ts
··· 1 + import { Class } from '../Class'; 2 + import { Engine, ScrollPreventionMode } from '../Engine'; 3 + import { GlobalCoordinates } from '../Math/global-coordinates'; 4 + import { vec, Vector } from '../Math/vector'; 5 + import { PointerEvent } from './PointerEvent'; 6 + import { WheelEvent } from './WheelEvent'; 7 + import { PointerAbstraction } from './PointerAbstraction'; 8 + 9 + import { WheelDeltaMode } from './WheelDeltaMode'; 10 + import { PointerSystem } from './PointerSystem'; 11 + import { NativePointerButton } from './NativePointerButton'; 12 + import { PointerButton } from './PointerButton'; 13 + import { Util } from '..'; 14 + import { PointerType } from './PointerType'; 15 + 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 + /** 23 + * The PointerEventProcessor is responsible for collecting all the events from the canvas and transforming them into GlobalCoordinates 24 + */ 25 + export class PointerEventReceiver extends Class { 26 + public primary: PointerAbstraction = new PointerAbstraction(); 27 + 28 + private _activeNativePointerIdsToNormalized = new Map<number, number>(); 29 + public lastFramePointerPosition = new Map<number, Vector>(); 30 + public currentFramePointerPositions = new Map<number, Vector>(); 31 + 32 + public currentFramePointerDown = new Map<number, boolean>(); 33 + public lastFramePointerDown = new Map<number, boolean>(); 34 + 35 + public currentFrameDown: PointerEvent[] = []; 36 + public currentFrameUp: PointerEvent[] = []; 37 + public currentFrameMove: PointerEvent[] = []; 38 + public currentFrameCancel: PointerEvent[] = []; 39 + public currentFrameWheel: WheelEvent[] = []; 40 + 41 + constructor(public readonly target: GlobalEventHandlers & EventTarget, public engine: Engine) { 42 + super(); 43 + } 44 + 45 + private _pointers: PointerAbstraction[] = [this.primary]; 46 + /** 47 + * Locates a specific pointer by id, creates it if it doesn't exist 48 + * @param index 49 + */ 50 + public at(index: number): PointerAbstraction { 51 + if (index >= this._pointers.length) { 52 + // Ensure there is a pointer to retrieve 53 + for (let i = this._pointers.length - 1, max = index; i < max; i++) { 54 + this._pointers.push(new PointerAbstraction()); 55 + } 56 + } 57 + return this._pointers[index]; 58 + } 59 + 60 + /** 61 + * The number of pointers currently being tracked by excalibur 62 + */ 63 + public count(): number { 64 + return this._pointers.length; 65 + } 66 + 67 + /** 68 + * Is the specified pointer id down this frame 69 + * @param pointerId 70 + */ 71 + public isDown(pointerId: number) { 72 + return this.currentFramePointerDown.get(pointerId) ?? false; 73 + } 74 + 75 + /** 76 + * Was the specified pointer id down last frame 77 + * @param pointerId 78 + */ 79 + public wasDown(pointerId: number) { 80 + return this.lastFramePointerDown.get(pointerId) ?? false; 81 + } 82 + 83 + /** 84 + * Whether the Pointer is currently dragging. 85 + */ 86 + public isDragging(pointerId: number): boolean { 87 + return this.isDown(pointerId); 88 + } 89 + 90 + /** 91 + * Whether the Pointer just started dragging. 92 + */ 93 + public isDragStart(pointerId: number): boolean { 94 + return this.isDown(pointerId) && !this.wasDown(pointerId); 95 + } 96 + 97 + /** 98 + * Whether the Pointer just ended dragging. 99 + */ 100 + public isDragEnd(pointerId: number): boolean { 101 + return !this.isDown(pointerId) && this.wasDown(pointerId); 102 + } 103 + 104 + on(event: 'move', handler: (event: PointerEvent) => void): void; 105 + on(event: 'down', handler: (event: PointerEvent) => void): void; 106 + on(event: 'up', handler: (event: PointerEvent) => void): void; 107 + on(event: 'wheel', handler: (event: WheelEvent) => void): void; 108 + on(event: string, handler: (event: any) => void): void { 109 + super.on(event, handler); 110 + } 111 + 112 + once(event: 'move', handler: (event: PointerEvent) => void): void; 113 + once(event: 'down', handler: (event: PointerEvent) => void): void; 114 + once(event: 'up', handler: (event: PointerEvent) => void): void; 115 + once(event: 'wheel', handler: (event: WheelEvent) => void): void; 116 + once(event: string, handler: (event: any) => void): void { 117 + super.once(event, handler); 118 + } 119 + 120 + off(event: 'move', handler?: (event: PointerEvent) => void): void; 121 + off(event: 'down', handler?: (event: PointerEvent) => void): void; 122 + off(event: 'up', handler?: (event: PointerEvent) => void): void; 123 + off(event: 'wheel', handler?: (event: WheelEvent) => void): void; 124 + off(event: string, handler?: (event: any) => void): void { 125 + super.off(event, handler); 126 + } 127 + 128 + /** 129 + * Called internally by excalibur 130 + * 131 + * Updates the current frame pointer info and emits raw pointer events 132 + * 133 + * This does not emit events to entities, see PointerSystem 134 + */ 135 + public update() { 136 + this.lastFramePointerDown = new Map(this.currentFramePointerDown); 137 + this.lastFramePointerPosition = new Map(this.currentFramePointerPositions); 138 + 139 + for (const event of this.currentFrameDown) { 140 + this.emit('down', event); 141 + const pointer = this.at(event.pointerId); 142 + pointer.emit('down', event); 143 + this.primary.emit('pointerdown', event); 144 + } 145 + 146 + for (const event of this.currentFrameUp) { 147 + this.emit('up', event); 148 + const pointer = this.at(event.pointerId); 149 + pointer.emit('up', event); 150 + } 151 + 152 + for (const event of this.currentFrameMove) { 153 + this.emit('move', event); 154 + const pointer = this.at(event.pointerId); 155 + pointer.emit('move', event); 156 + } 157 + 158 + for (const event of this.currentFrameCancel) { 159 + this.emit('cancel', event); 160 + const pointer = this.at(event.pointerId); 161 + pointer.emit('cancel', event); 162 + } 163 + 164 + for (const event of this.currentFrameWheel) { 165 + this.emit('wheel', event); 166 + this.primary.emit('pointerwheel', event); 167 + } 168 + } 169 + 170 + /** 171 + * Clears the current frame event and pointer data 172 + */ 173 + public clear() { 174 + for (const event of this.currentFrameUp) { 175 + this.currentFramePointerPositions.delete(event.pointerId); 176 + const ids = this._activeNativePointerIdsToNormalized.entries(); 177 + for (const [native, normalized] of ids) { 178 + if (normalized === event.pointerId) { 179 + this._activeNativePointerIdsToNormalized.delete(native); 180 + } 181 + } 182 + } 183 + this.currentFrameDown.length = 0; 184 + this.currentFrameUp.length = 0; 185 + this.currentFrameMove.length = 0; 186 + this.currentFrameCancel.length = 0; 187 + this.currentFrameWheel.length = 0; 188 + } 189 + 190 + private _boundHandle = this._handle.bind(this); 191 + private _boundWheel = this._handleWheel.bind(this); 192 + /** 193 + * Initializes the pointer event receiver so that it can start listening to native 194 + * browser events. 195 + */ 196 + public init() { 197 + // Disabling the touch action avoids browser/platform gestures from firing on the canvas 198 + this.engine.canvas.style.touchAction = 'none'; 199 + // Preferred pointer events 200 + if (window.PointerEvent) { 201 + this.target.addEventListener('pointerdown', this._boundHandle); 202 + this.target.addEventListener('pointerup', this._boundHandle); 203 + this.target.addEventListener('pointermove', this._boundHandle); 204 + this.target.addEventListener('pointercancel', this._boundHandle); 205 + } else { 206 + // Touch Events 207 + this.target.addEventListener('touchstart', this._boundHandle); 208 + this.target.addEventListener('touchend', this._boundHandle); 209 + this.target.addEventListener('touchmove', this._boundHandle); 210 + this.target.addEventListener('touchcancel', this._boundHandle); 211 + 212 + // Mouse Events 213 + this.target.addEventListener('mousedown', this._boundHandle); 214 + this.target.addEventListener('mouseup', this._boundHandle); 215 + this.target.addEventListener('mousemove', this._boundHandle); 216 + } 217 + 218 + // MDN MouseWheelEvent 219 + const wheelOptions = { 220 + passive: !( 221 + this.engine.pageScrollPreventionMode === ScrollPreventionMode.All || 222 + this.engine.pageScrollPreventionMode === ScrollPreventionMode.Canvas 223 + ) 224 + }; 225 + if ('onwheel' in document.createElement('div')) { 226 + // Modern Browsers 227 + this.target.addEventListener('wheel', this._boundWheel, wheelOptions); 228 + } else if (document.onmousewheel !== undefined) { 229 + // Webkit and IE 230 + this.target.addEventListener('mousewheel', this._boundWheel, wheelOptions); 231 + } else { 232 + // Remaining browser and older Firefox 233 + this.target.addEventListener('MozMousePixelScroll', this._boundWheel, wheelOptions); 234 + } 235 + } 236 + 237 + public detach() { 238 + // Preferred pointer events 239 + if (window.PointerEvent) { 240 + this.target.removeEventListener('pointerdown', this._boundHandle); 241 + this.target.removeEventListener('pointerup', this._boundHandle); 242 + this.target.removeEventListener('pointermove', this._boundHandle); 243 + this.target.removeEventListener('pointercancel', this._boundHandle); 244 + } else { 245 + // Touch Events 246 + this.target.removeEventListener('touchstart', this._boundHandle); 247 + this.target.removeEventListener('touchend', this._boundHandle); 248 + this.target.removeEventListener('touchmove', this._boundHandle); 249 + this.target.removeEventListener('touchcancel', this._boundHandle); 250 + 251 + // Mouse Events 252 + this.target.removeEventListener('mousedown', this._boundHandle); 253 + this.target.removeEventListener('mouseup', this._boundHandle); 254 + this.target.removeEventListener('mousemove', this._boundHandle); 255 + } 256 + 257 + if ('onwheel' in document.createElement('div')) { 258 + // Modern Browsers 259 + this.target.removeEventListener('wheel', this._boundWheel); 260 + } else if (document.onmousewheel !== undefined) { 261 + // Webkit and IE 262 + this.target.addEventListener('mousewheel', this._boundWheel); 263 + } else { 264 + // Remaining browser and older Firefox 265 + this.target.addEventListener('MozMousePixelScroll', this._boundWheel); 266 + } 267 + } 268 + 269 + /** 270 + * Take native pointer id and map it to index in active pointers 271 + * @param nativePointerId 272 + * @returns 273 + */ 274 + private _normalizePointerId(nativePointerId: number) { 275 + // Add to the the native pointer set id 276 + this._activeNativePointerIdsToNormalized.set(nativePointerId, -1); 277 + 278 + // Native pointer ids in ascending order 279 + const currentPointerIds = Array.from(this._activeNativePointerIdsToNormalized.keys()).sort((a, b) => a - b); 280 + 281 + // The index into sorted ids will be the new id, will always have an id 282 + const id = currentPointerIds.findIndex(p => p === nativePointerId); 283 + 284 + // Save the mapping so we can reverse it later 285 + this._activeNativePointerIdsToNormalized.set(nativePointerId, id); 286 + 287 + // ignore pointer because game isn't watching 288 + return id; 289 + } 290 + 291 + /** 292 + * Responsible for handling and parsing pointer events 293 + */ 294 + private _handle(ev: NativeTouchEvent | NativePointerEvent | NativeMouseEvent) { 295 + ev.preventDefault(); 296 + const eventCoords = new Map<number, GlobalCoordinates>(); 297 + let button: PointerButton; 298 + let pointerType: PointerType; 299 + if (ev instanceof TouchEvent) { 300 + button = PointerButton.Unknown; 301 + pointerType = PointerType.Touch; 302 + // https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent 303 + for (let i = 0; i < ev.changedTouches.length; i++) { 304 + const touch = ev.changedTouches[i]; 305 + const coordinates = GlobalCoordinates.fromPagePosition(touch.pageX, touch.pageY, this.engine); 306 + const nativePointerId = i + 1; 307 + const pointerId = this._normalizePointerId(nativePointerId); 308 + this.currentFramePointerPositions.set(pointerId, coordinates.worldPos); 309 + eventCoords.set(pointerId, coordinates); 310 + } 311 + } else { 312 + button = this._nativeButtonToPointerButton(ev.button); 313 + pointerType = PointerType.Mouse; 314 + const coordinates = GlobalCoordinates.fromPagePosition(ev.pageX, ev.pageY, this.engine); 315 + let nativePointerId = 1; 316 + if (ev instanceof window.PointerEvent) { 317 + nativePointerId = ev.pointerId; 318 + pointerType = this._stringToPointerType(ev.pointerType); 319 + } 320 + const pointerId = this._normalizePointerId(nativePointerId); 321 + this.currentFramePointerPositions.set(pointerId, coordinates.worldPos); 322 + eventCoords.set(pointerId, coordinates); 323 + } 324 + 325 + for (const [pointerId, coord] of eventCoords.entries()) { 326 + switch (ev.type) { 327 + case 'mousedown': 328 + case 'pointerdown': 329 + case 'touchstart': 330 + this.currentFrameDown.push(new PointerEvent('down', pointerId, button, pointerType, coord, ev)); 331 + this.currentFramePointerDown.set(pointerId, true); 332 + break; 333 + case 'mouseup': 334 + case 'pointerup': 335 + case 'touchend': 336 + this.currentFrameUp.push(new PointerEvent('up', pointerId, button, pointerType, coord, ev)); 337 + this.currentFramePointerDown.set(pointerId, false); 338 + break; 339 + case 'mousemove': 340 + case 'pointermove': 341 + case 'touchmove': 342 + this.currentFrameMove.push(new PointerEvent('move', pointerId, button, pointerType, coord, ev)); 343 + break; 344 + case 'touchcancel': 345 + case 'pointercance': 346 + this.currentFrameCancel.push(new PointerEvent('cancel', pointerId, button, pointerType, coord, ev)); 347 + break; 348 + } 349 + } 350 + } 351 + 352 + private _handleWheel(ev: NativeWheelEvent) { 353 + // Should we prevent page scroll because of this event 354 + if ( 355 + this.engine.pageScrollPreventionMode === ScrollPreventionMode.All || 356 + (this.engine.pageScrollPreventionMode === ScrollPreventionMode.Canvas && ev.target === this.engine.canvas) 357 + ) { 358 + ev.preventDefault(); 359 + } 360 + const screen = this.engine.screen.pageToScreenCoordinates(vec(ev.pageX, ev.pageY)); 361 + const world = this.engine.screen.screenToWorldCoordinates(screen); 362 + 363 + /** 364 + * A constant used to normalize wheel events across different browsers 365 + * 366 + * This normalization factor is pulled from 367 + * https://developer.mozilla.org/en-US/docs/Web/Events/wheel#Listening_to_this_event_across_browser 368 + */ 369 + const ScrollWheelNormalizationFactor = -1 / 40; 370 + 371 + const deltaX = ev.deltaX || ev.wheelDeltaX * ScrollWheelNormalizationFactor || 0; 372 + const deltaY = 373 + ev.deltaY || ev.wheelDeltaY * ScrollWheelNormalizationFactor || ev.wheelDelta * ScrollWheelNormalizationFactor || ev.detail || 0; 374 + const deltaZ = ev.deltaZ || 0; 375 + let deltaMode = WheelDeltaMode.Pixel; 376 + 377 + if (ev.deltaMode) { 378 + if (ev.deltaMode === 1) { 379 + deltaMode = WheelDeltaMode.Line; 380 + } else if (ev.deltaMode === 2) { 381 + deltaMode = WheelDeltaMode.Page; 382 + } 383 + } 384 + 385 + const we = new WheelEvent(world.x, world.y, ev.pageX, ev.pageY, screen.x, screen.y, 0, deltaX, deltaY, deltaZ, deltaMode, ev); 386 + this.currentFrameWheel.push(we); 387 + } 388 + 389 + /** 390 + * Triggers an excalibur pointer event in a world space pos 391 + * 392 + * Useful for testing pointers in excalibur 393 + * @param type 394 + * @param pos 395 + */ 396 + public triggerEvent(type: 'down' | 'up' | 'move' | 'cancel', pos: Vector) { 397 + const page = this.engine.screen.worldToPageCoordinates(pos); 398 + // Send an event to the event receiver 399 + this._handle(new window.PointerEvent('pointer' + type, { 400 + pointerId: 0, 401 + clientX: page.x, 402 + clientY: page.y 403 + })); 404 + 405 + // Force update pointer system 406 + const pointerSystem = this.engine.currentScene.world.systemManager.get(PointerSystem); 407 + const transformEntities = this.engine.currentScene.world.queryManager.createQuery(pointerSystem.types); 408 + pointerSystem.update(transformEntities.getEntities()); 409 + } 410 + 411 + private _nativeButtonToPointerButton(s: NativePointerButton): PointerButton { 412 + switch (s) { 413 + case NativePointerButton.NoButton: 414 + return PointerButton.NoButton; 415 + case NativePointerButton.Left: 416 + return PointerButton.Left; 417 + case NativePointerButton.Middle: 418 + return PointerButton.Middle; 419 + case NativePointerButton.Right: 420 + return PointerButton.Right; 421 + case NativePointerButton.Unknown: 422 + return PointerButton.Unknown; 423 + default: 424 + return Util.fail(s); 425 + } 426 + } 427 + 428 + private _stringToPointerType(s: string) { 429 + switch (s) { 430 + case 'touch': 431 + return PointerType.Touch; 432 + case 'mouse': 433 + return PointerType.Mouse; 434 + case 'pen': 435 + return PointerType.Pen; 436 + default: 437 + return PointerType.Unknown; 438 + } 439 + } 440 + }
-381
src/engine/Input/PointerEvents.ts
··· 1 - import { Actor } from '../Actor'; 2 - import { GlobalCoordinates } from '../Math/global-coordinates'; 3 - import { Vector } from '../Math/vector'; 4 - import { Pointer, PointerType } from './Pointer'; 5 - import { GameEvent } from '../Events'; 6 - 7 - /** 8 - * Native browser button enumeration 9 - */ 10 - export enum NativePointerButton { 11 - NoButton = -1, 12 - Left = 0, 13 - Middle = 1, 14 - Right = 2, 15 - Unknown = 3 16 - } 17 - 18 - /** 19 - * The mouse button being pressed. 20 - */ 21 - export enum PointerButton { 22 - Left = 'Left', 23 - Middle = 'Middle', 24 - Right = 'Right', 25 - Unknown = 'Unknown', 26 - NoButton = 'NoButton' 27 - } 28 - 29 - export enum WheelDeltaMode { 30 - Pixel = 'Pixel', 31 - Line = 'Line', 32 - Page = 'Page' 33 - } 34 - 35 - /** 36 - * Type that indicates Excalibur's valid synthetic pointer events 37 - */ 38 - export type PointerEventName = 39 - | 'pointerdragstart' 40 - | 'pointerdragend' 41 - | 'pointerdragmove' 42 - | 'pointerdragenter' 43 - | 'pointerdragleave' 44 - | 'pointermove' 45 - | 'pointerenter' 46 - | 'pointerleave' 47 - | 'pointerup' 48 - | 'pointerdown'; 49 - 50 - /** 51 - * Pointer events 52 - * 53 - * Represents a mouse, touch, or stylus event. See [[Pointers]] for more information on 54 - * handling pointer input. 55 - * 56 - * For mouse-based events, you can inspect [[PointerEvent.button]] to see what button was pressed. 57 - */ 58 - export class PointerEvent extends GameEvent<Actor> { 59 - protected _name: string; 60 - 61 - public get name() { 62 - return this._name; 63 - } 64 - 65 - /** The world coordinates of the event. */ 66 - public get worldPos(): Vector { 67 - return this.coordinates.worldPos.clone(); 68 - } 69 - 70 - /** The page coordinates of the event. */ 71 - public get pagePos(): Vector { 72 - return this.coordinates.pagePos.clone(); 73 - } 74 - 75 - /** The screen coordinates of the event. */ 76 - public get screenPos(): Vector { 77 - return this.coordinates.screenPos.clone(); 78 - } 79 - 80 - /** 81 - * @param coordinates The [[GlobalCoordinates]] of the event 82 - * @param pointer The [[Pointer]] of the event 83 - * @param index The index of the pointer (zero-based) 84 - * @param pointerType The type of pointer 85 - * @param button The button pressed (if [[PointerType.Mouse]]) 86 - * @param ev The raw DOM event being handled 87 - */ 88 - constructor( 89 - protected coordinates: GlobalCoordinates, 90 - public pointer: Pointer, 91 - public index: number, 92 - public pointerType: PointerType, 93 - public button: PointerButton, 94 - public ev: any 95 - ) { 96 - super(); 97 - } 98 - 99 - public get pos(): Vector { 100 - return this.coordinates.worldPos.clone(); 101 - } 102 - 103 - public _canceled = false; 104 - 105 - /** 106 - * Cancels pointer event propogation, event will not be transmitted to any other actors 107 - */ 108 - public cancel() { 109 - this._canceled = true; 110 - } 111 - 112 - /** 113 - * 114 - * @returns If the event is canceled it will no longer be transmitted to any other actors 115 - */ 116 - public isCanceled() { 117 - return this._canceled; 118 - } 119 - 120 - public propagate(actor: Actor): void { 121 - this.doAction(actor); 122 - 123 - if (this.bubbles && !this.isCanceled() && actor.parent) { 124 - this.propagate(actor.parent as Actor); // TODO not true 125 - } 126 - } 127 - 128 - /** 129 - * Action, that calls when event happens 130 - */ 131 - public doAction(actor: Actor): void { 132 - if (actor) { 133 - this._onActionStart(actor); 134 - actor.emit(this._name, this); 135 - this._onActionEnd(actor); 136 - } 137 - } 138 - 139 - protected _onActionStart(_actor?: Actor) { 140 - // to be rewritten 141 - } 142 - 143 - protected _onActionEnd(_actor?: Actor) { 144 - // to be rewritten 145 - } 146 - } 147 - 148 - export class PointerEventFactory<T extends PointerEvent> { 149 - constructor( 150 - protected _pointerEventType: new ( 151 - coordinates: GlobalCoordinates, 152 - pointer: Pointer, 153 - index: number, 154 - pointerType: PointerType, 155 - button: PointerButton, 156 - ev: any 157 - ) => T 158 - ) {} 159 - 160 - /** 161 - * Create specific PointerEvent 162 - */ 163 - public create( 164 - coordinates: GlobalCoordinates, 165 - pointer: Pointer, 166 - index: number, 167 - pointerType: PointerType, 168 - button: PointerButton, 169 - ev: any 170 - ): T { 171 - return new this._pointerEventType(coordinates, pointer, index, pointerType, button, ev); 172 - } 173 - } 174 - 175 - export class PointerDragEvent extends PointerEvent {} 176 - 177 - export class PointerUpEvent extends PointerEvent { 178 - protected _name = 'pointerup'; 179 - 180 - protected _onActionEnd(actor: Actor) { 181 - const pointer = this.pointer; 182 - 183 - if (pointer.isDragEnd && actor.capturePointer.captureDragEvents) { 184 - actor.eventDispatcher.emit('pointerdragend', this); 185 - } 186 - } 187 - } 188 - 189 - export class PointerDownEvent extends PointerEvent { 190 - protected _name = 'pointerdown'; 191 - 192 - protected _onActionEnd(actor: Actor) { 193 - if (this.pointer.isDragStart && actor.capturePointer.captureDragEvents) { 194 - actor.eventDispatcher.emit('pointerdragstart', this); 195 - } 196 - } 197 - } 198 - 199 - export class PointerMoveEvent extends PointerEvent { 200 - protected _name = 'pointermove'; 201 - 202 - public propagate(actor: Actor): void { 203 - // If the actor was under the pointer last frame, but not this one it left 204 - // if (this.pointer.wasActorUnderPointer(actor) && !this.pointer.isActorUnderPointer(actor)) { 205 - // this._onActorLeave(actor); 206 - // return; 207 - // } 208 - 209 - if (this.pointer.isActorAliveUnderPointer(actor)) { 210 - this.doAction(actor); 211 - 212 - if (this.bubbles && !this.isCanceled() && actor.parent) { 213 - this.propagate(actor.parent as Actor); // TODO not true 214 - } 215 - } 216 - } 217 - 218 - protected _onActionStart(actor: Actor) { 219 - if (!actor.capturePointer.captureMoveEvents) { 220 - return; 221 - } 222 - 223 - // In the case this is new 224 - // if (this.pointer.checkActorUnderPointer(actor) && !this.pointer.wasActorUnderPointer(actor)) { 225 - // this._onActorEnter(actor); 226 - // } 227 - 228 - if (this.pointer.isDragging && actor.capturePointer.captureDragEvents) { 229 - actor.eventDispatcher.emit('pointerdragmove', this); 230 - } 231 - } 232 - 233 - // private _onActorEnter(actor: Actor) { 234 - // const pe = createPointerEventByName('enter', this.coordinates, this.pointer, this.index, this.pointerType, this.button, this.ev); 235 - // pe.propagate(actor); 236 - // this.pointer.addActorUnderPointer(actor); 237 - 238 - // if (this.pointer.isDragging) { 239 - // this.pointer.dragTarget = actor; 240 - // } 241 - // } 242 - 243 - // private _onActorLeave(actor: Actor) { 244 - // const pe = createPointerEventByName('leave', this.coordinates, this.pointer, this.index, this.pointerType, this.button, this.ev); 245 - // pe.propagate(actor); 246 - // this.pointer.removeActorUnderPointer(actor); 247 - // } 248 - } 249 - 250 - export class PointerEnterEvent extends PointerEvent { 251 - protected _name = 'pointerenter'; 252 - 253 - protected _onActionStart(actor: Actor) { 254 - if (!actor.capturePointer.captureMoveEvents) { 255 - return; 256 - } 257 - } 258 - 259 - protected _onActionEnd(actor: Actor) { 260 - const pointer = this.pointer; 261 - 262 - if (pointer.isDragging && actor.capturePointer.captureDragEvents) { 263 - actor.eventDispatcher.emit('pointerdragenter', this); 264 - } 265 - } 266 - } 267 - 268 - export class PointerLeaveEvent extends PointerEvent { 269 - protected _name = 'pointerleave'; 270 - 271 - protected _onActionStart(actor: Actor) { 272 - if (!actor.capturePointer.captureMoveEvents) { 273 - return; 274 - } 275 - } 276 - 277 - protected _onActionEnd(actor: Actor) { 278 - const pointer = this.pointer; 279 - 280 - if (pointer.isDragging && actor.capturePointer.captureDragEvents) { 281 - actor.eventDispatcher.emit('pointerdragleave', this); 282 - } 283 - } 284 - } 285 - 286 - export class PointerCancelEvent extends PointerEvent { 287 - protected _name = 'pointercancel'; 288 - } 289 - /** 290 - * Wheel Events 291 - * 292 - * Represents a mouse wheel event. See [[Pointers]] for more information on 293 - * handling point input. 294 - */ 295 - export class WheelEvent extends GameEvent<Actor> { 296 - /** 297 - * @param x The `x` coordinate of the event (in world coordinates) 298 - * @param y The `y` coordinate of the event (in world coordinates) 299 - * @param pageX The `x` coordinate of the event (in document coordinates) 300 - * @param pageY The `y` coordinate of the event (in document coordinates) 301 - * @param screenX The `x` coordinate of the event (in screen coordinates) 302 - * @param screenY The `y` coordinate of the event (in screen coordinates) 303 - * @param index The index of the pointer (zero-based) 304 - * @param deltaX The type of pointer 305 - * @param deltaY The type of pointer 306 - * @param deltaZ The type of pointer 307 - * @param deltaMode The type of movement [[WheelDeltaMode]] 308 - * @param ev The raw DOM event being handled 309 - */ 310 - constructor( 311 - public x: number, 312 - public y: number, 313 - public pageX: number, 314 - public pageY: number, 315 - public screenX: number, 316 - public screenY: number, 317 - public index: number, 318 - public deltaX: number, 319 - public deltaY: number, 320 - public deltaZ: number, 321 - public deltaMode: WheelDeltaMode, 322 - public ev: any 323 - ) { 324 - super(); 325 - } 326 - 327 - private _isCanceled = false; 328 - 329 - /** 330 - * Cancels pointer event propogation, event will not be transmitted to any other actors 331 - */ 332 - public cancel() { 333 - this._isCanceled = true; 334 - } 335 - 336 - /** 337 - * 338 - * @returns If the event is canceled it will no longer be transmitted to any other actors 339 - */ 340 - public isCanceled() { 341 - return this._isCanceled; 342 - } 343 - } 344 - 345 - /** 346 - * 347 - */ 348 - export function createPointerEventByName( 349 - eventName: string, 350 - coordinates: GlobalCoordinates, 351 - pointer: Pointer, 352 - index: number, 353 - pointerType: PointerType, 354 - button: PointerButton, 355 - ev: any 356 - ) { 357 - let factory: PointerEventFactory<PointerEvent>; 358 - 359 - switch (eventName) { 360 - case 'up': 361 - factory = new PointerEventFactory(<any>PointerUpEvent); 362 - break; 363 - case 'down': 364 - factory = new PointerEventFactory(<any>PointerDownEvent); 365 - break; 366 - case 'move': 367 - factory = new PointerEventFactory(<any>PointerMoveEvent); 368 - break; 369 - case 'cancel': 370 - factory = new PointerEventFactory(<any>PointerCancelEvent); 371 - break; 372 - case 'enter': 373 - factory = new PointerEventFactory(<any>PointerEnterEvent); 374 - break; 375 - case 'leave': 376 - factory = new PointerEventFactory(<any>PointerLeaveEvent); 377 - break; 378 - } 379 - 380 - return factory.create(coordinates, pointer, index, pointerType, button, ev); 381 - }
+16
src/engine/Input/PointerScope.ts
··· 1 + /** 2 + * Determines the scope of handling mouse/touch events. See [[Pointers]] for more information. 3 + */ 4 + 5 + export enum PointerScope { 6 + /** 7 + * Handle events on the `canvas` element only. Events originating outside the 8 + * `canvas` will not be handled. 9 + */ 10 + Canvas = 'Canvas', 11 + 12 + /** 13 + * Handles events on the entire document. All events will be handled by Excalibur. 14 + */ 15 + Document = 'Document' 16 + }
+238
src/engine/Input/PointerSystem.ts
··· 1 + import { ColliderComponent } from '../Collision/ColliderComponent'; 2 + import { Engine } from '../Engine'; 3 + import { System, TransformComponent, SystemType, Entity } from '../EntityComponentSystem'; 4 + import { GraphicsComponent } from '../Graphics/GraphicsComponent'; 5 + import { Scene } from '../Scene'; 6 + import { PointerComponent } from './PointerComponent'; 7 + import { PointerEventReceiver } from './PointerEventReceiver'; 8 + import { PointerEvent } from './PointerEvent'; 9 + 10 + /** 11 + * The PointerSystem is responsible for dispatching pointer events to entities 12 + * that need them. 13 + * 14 + * The PointerSystem can be optionally configured by the [[PointerComponent]], by default Entities use 15 + * the [[Collider]]'s shape for pointer events. 16 + */ 17 + export class PointerSystem extends System<TransformComponent | PointerComponent> { 18 + public readonly types = ['ex.transform', 'ex.pointer'] as const; 19 + public readonly systemType = SystemType.Update; 20 + public priority = -1; 21 + 22 + private _engine: Engine; 23 + private _receiver: PointerEventReceiver; 24 + 25 + public lastFrameEntityToPointers = new Map<number, number[]>(); 26 + public currentFrameEntityToPointers = new Map<number, number[]>(); 27 + 28 + public initialize(scene: Scene): void { 29 + this._engine = scene.engine; 30 + this._receiver = this._engine.input.pointers; 31 + } 32 + 33 + public sort(a: Entity, b: Entity) { 34 + // Sort from high to low, because things on 'top' receive the pointer events first 35 + return b.get(TransformComponent).z - a.get(TransformComponent).z; 36 + } 37 + 38 + public entityCurrentlyUnderPointer(entity: Entity, pointerId: number) { 39 + return this.currentFrameEntityToPointers.has(entity.id) && 40 + this.currentFrameEntityToPointers.get(entity.id).includes(pointerId); 41 + } 42 + 43 + public entityWasUnderPointer(entity: Entity, pointerId: number) { 44 + return this.lastFrameEntityToPointers.has(entity.id) && 45 + this.lastFrameEntityToPointers.get(entity.id).includes(pointerId); 46 + } 47 + 48 + public entered(entity: Entity, pointerId: number) { 49 + return this.entityCurrentlyUnderPointer(entity, pointerId) && 50 + !this.lastFrameEntityToPointers.has(entity.id); 51 + } 52 + 53 + public left(entity: Entity, pointerId: number) { 54 + return !this.currentFrameEntityToPointers.has(entity.id) && 55 + this.entityWasUnderPointer(entity, pointerId); 56 + } 57 + 58 + public addPointerToEntity(entity: Entity, pointerId: number) { 59 + if (!this.currentFrameEntityToPointers.has(entity.id)) { 60 + this.currentFrameEntityToPointers.set(entity.id, [pointerId]); 61 + return; 62 + } 63 + const pointers = this.currentFrameEntityToPointers.get(entity.id); 64 + this.currentFrameEntityToPointers.set(entity.id, pointers.concat(pointerId)); 65 + } 66 + 67 + public update(entities: Entity[]): void { 68 + // Locate all the pointer/entity mappings 69 + this._processPointerToEntity(entities); 70 + 71 + // Dispatch pointer events on entities 72 + this._dispatchEvents(entities); 73 + 74 + // Clear last frame's events 75 + this._receiver.update(); 76 + this.lastFrameEntityToPointers.clear(); 77 + this.lastFrameEntityToPointers = new Map<number, number[]>(this.currentFrameEntityToPointers); 78 + this.currentFrameEntityToPointers.clear(); 79 + this._receiver.clear(); 80 + } 81 + 82 + private _processPointerToEntity(entities: Entity[]) { 83 + let transform: TransformComponent; 84 + let collider: ColliderComponent; 85 + let graphics: GraphicsComponent; 86 + let pointer: PointerComponent; 87 + 88 + // TODO probably a spatial partition optimization here to quickly query bounds for pointer 89 + // Pre-process find entities under pointers 90 + for (const entity of entities) { 91 + transform = entity.get(TransformComponent); 92 + pointer = entity.get(PointerComponent) ?? new PointerComponent; 93 + // Check collider contains pointer 94 + collider = entity.get(ColliderComponent); 95 + if (collider && pointer.useColliderShape) { 96 + const geom = collider.get(); 97 + if (geom) { 98 + for (const [pointerId, pos] of this._receiver.currentFramePointerPositions.entries()) { 99 + if (geom.contains(pos)) { 100 + this.addPointerToEntity(entity, pointerId); 101 + } 102 + } 103 + } 104 + } 105 + 106 + // Check graphics contains pointer 107 + graphics = entity.get(GraphicsComponent); 108 + if (graphics && pointer.useGraphicsBounds) { 109 + const graphicBounds = graphics.localBounds.transform(transform.getGlobalMatrix()); 110 + for (const [pointerId, pos] of this._receiver.currentFramePointerPositions.entries()) { 111 + if (graphicBounds.contains(pos)) { 112 + this.addPointerToEntity(entity, pointerId); 113 + } 114 + } 115 + } 116 + } 117 + } 118 + 119 + private _processDownAndEmit(entity: Entity): Map<number, PointerEvent> { 120 + const lastDownPerPointer = new Map<number, PointerEvent>(); 121 + // Loop through down and dispatch to entities 122 + for (const event of this._receiver.currentFrameDown) { 123 + if (event.active && entity.active && this.entityCurrentlyUnderPointer(entity, event.pointerId)) { 124 + entity.events.emit('pointerdown', event as any); 125 + if (this._receiver.isDragStart(event.pointerId)) { 126 + entity.events.emit('pointerdragstart', event as any); 127 + } 128 + } 129 + lastDownPerPointer.set(event.pointerId, event); 130 + } 131 + return lastDownPerPointer; 132 + } 133 + 134 + private _processUpAndEmit(entity: Entity): Map<number, PointerEvent> { 135 + const lastUpPerPointer = new Map<number, PointerEvent>(); 136 + // Loop through up and dispatch to entities 137 + for (const event of this._receiver.currentFrameUp) { 138 + if (event.active && entity.active && this.entityCurrentlyUnderPointer(entity, event.pointerId)) { 139 + entity.events.emit('pointerup', event as any); 140 + if (this._receiver.isDragEnd(event.pointerId)) { 141 + entity.events.emit('pointerdragend', event as any); 142 + } 143 + } 144 + lastUpPerPointer.set(event.pointerId, event); 145 + } 146 + return lastUpPerPointer; 147 + } 148 + 149 + private _processMoveAndEmit(entity: Entity): Map<number, PointerEvent> { 150 + const lastMovePerPointer = new Map<number, PointerEvent>(); 151 + // Loop through move and dispatch to entities 152 + for (const event of this._receiver.currentFrameMove) { 153 + if (event.active && entity.active && this.entityCurrentlyUnderPointer(entity, event.pointerId)) { 154 + // move 155 + entity.events.emit('pointermove', event as any); 156 + 157 + if (this._receiver.isDragging(event.pointerId)) { 158 + entity.events.emit('pointerdragmove', event as any); 159 + } 160 + } 161 + lastMovePerPointer.set(event.pointerId, event); 162 + } 163 + return lastMovePerPointer; 164 + } 165 + 166 + private _processEnterLeaveAndEmit(entity: Entity, lastUpDownMoveEvents: PointerEvent[]) { 167 + // up, down, and move are considered for enter and leave 168 + for (const event of lastUpDownMoveEvents) { 169 + // enter 170 + if (event.active && entity.active && this.entered(entity, event.pointerId)) { 171 + entity.events.emit('pointerenter', event as any); 172 + if (this._receiver.isDragging(event.pointerId)) { 173 + entity.events.emit('pointerdragenter', event as any); 174 + } 175 + break; 176 + } 177 + if (event.active && entity.active && 178 + // leave can happen on move 179 + (this.left(entity, event.pointerId) || 180 + // or leave can happen on pointer up 181 + (this.entityCurrentlyUnderPointer(entity, event.pointerId) && event.type === 'up'))) { 182 + entity.events.emit('pointerleave', event as any); 183 + if (this._receiver.isDragging(event.pointerId)) { 184 + entity.events.emit('pointerdragleave', event as any); 185 + } 186 + break; 187 + } 188 + } 189 + } 190 + 191 + private _processCancelAndEmit(entity: Entity) { 192 + // cancel 193 + for (const event of this._receiver.currentFrameCancel) { 194 + if (event.active && entity.active && this.entityCurrentlyUnderPointer(entity, event.pointerId)){ 195 + entity.events.emit('pointercancel', event as any); 196 + } 197 + } 198 + } 199 + 200 + private _processWheelAndEmit(entity: Entity) { 201 + // wheel 202 + for (const event of this._receiver.currentFrameWheel) { 203 + // Currently the wheel only fires under the primary pointer '0' 204 + if (event.active && entity.active && this.entityCurrentlyUnderPointer(entity, 0)) { 205 + entity.events.emit('pointerwheel', event as any); 206 + } 207 + } 208 + } 209 + 210 + private _dispatchEvents(entities: Entity[]) { 211 + const lastFrameEntities = new Set(this.lastFrameEntityToPointers.keys()); 212 + const currentFrameEntities = new Set(this.currentFrameEntityToPointers.keys()); 213 + // Filter preserves z order 214 + const entitiesWithEvents = entities.filter(e => lastFrameEntities.has(e.id) || currentFrameEntities.has(e.id)); 215 + let lastMovePerPointer: Map<number, PointerEvent>; 216 + let lastUpPerPointer: Map<number, PointerEvent>; 217 + let lastDownPerPointer: Map<number, PointerEvent>; 218 + // Dispatch events in entity z order 219 + for (const entity of entitiesWithEvents) { 220 + lastDownPerPointer = this._processDownAndEmit(entity); 221 + 222 + lastUpPerPointer = this._processUpAndEmit(entity); 223 + 224 + lastMovePerPointer = this._processMoveAndEmit(entity); 225 + 226 + const lastUpDownMoveEvents = [ 227 + ...lastMovePerPointer.values(), 228 + ...lastDownPerPointer.values(), 229 + ...lastUpPerPointer.values() 230 + ]; 231 + this._processEnterLeaveAndEmit(entity, lastUpDownMoveEvents); 232 + 233 + this._processCancelAndEmit(entity); 234 + 235 + this._processWheelAndEmit(entity); 236 + } 237 + } 238 + }
+9
src/engine/Input/PointerType.ts
··· 1 + /** 2 + * The type of pointer for a [[PointerEvent]]. 3 + */ 4 + export enum PointerType { 5 + Touch = 'Touch', 6 + Mouse = 'Mouse', 7 + Pen = 'Pen', 8 + Unknown = 'Unknown' 9 + }
-551
src/engine/Input/Pointers.ts
··· 1 - import { Class } from '../Class'; 2 - import { Engine, ScrollPreventionMode } from '../Engine'; 3 - import { Pointer, PointerType } from './Pointer'; 4 - import { 5 - PointerEvent, 6 - WheelEvent, 7 - NativePointerButton, 8 - PointerButton, 9 - WheelDeltaMode, 10 - createPointerEventByName, 11 - PointerLeaveEvent, 12 - PointerEnterEvent 13 - } from './PointerEvents'; 14 - import { GameEvent } from '../Events'; 15 - 16 - import * as Events from '../Events'; 17 - import * as Util from '../Util/Util'; 18 - import { vec } from '../Math/vector'; 19 - import { GlobalCoordinates } from '../Math/global-coordinates'; 20 - import { Vector } from '../Math/vector'; 21 - import { CapturePointer } from '../Traits/CapturePointer'; 22 - import { Actor } from '../Actor'; 23 - 24 - export type DOMPointerEvent = globalThis.PointerEvent; 25 - export type DOMWheelEvent = globalThis.WheelEvent; 26 - 27 - interface TouchEvent extends Event { 28 - altKey: boolean; 29 - changedTouches: Touch[]; 30 - ctrlKey: boolean; 31 - metaKey: boolean; 32 - shiftKey: boolean; 33 - targetTouches: Touch[]; 34 - touches: Touch[]; 35 - type: string; 36 - target: Element; 37 - } 38 - 39 - interface Touch { 40 - identifier: number; 41 - screenX: number; 42 - screenY: number; 43 - clientX: number; 44 - clientY: number; 45 - pageX: number; 46 - pageY: number; 47 - radiusX: number; 48 - radiusY: number; 49 - rotationAngle: number; 50 - force: number; 51 - target: Element; 52 - } 53 - 54 - /** 55 - * A constant used to normalize wheel events across different browsers 56 - * 57 - * This normalization factor is pulled from https://developer.mozilla.org/en-US/docs/Web/Events/wheel#Listening_to_this_event_across_browser 58 - */ 59 - const ScrollWheelNormalizationFactor = -1 / 40; 60 - 61 - /** 62 - * Handles pointer events (mouse, touch, stylus, etc.) and normalizes to 63 - * [W3C Pointer Events](http://www.w3.org/TR/pointerevents/). 64 - */ 65 - export class Pointers extends Class { 66 - private _engine: Engine; 67 - 68 - private _pointerDown: PointerEvent[] = []; 69 - private _pointerUp: PointerEvent[] = []; 70 - private _pointerMove: PointerEvent[] = []; 71 - private _pointerCancel: PointerEvent[] = []; 72 - private _wheel: WheelEvent[] = []; 73 - private _pointers: Pointer[] = []; 74 - private _activePointers: number[] = []; 75 - 76 - constructor(engine: Engine) { 77 - super(); 78 - 79 - this._engine = engine; 80 - this._pointers.push(new Pointer()); 81 - this._activePointers = [-1]; 82 - this.primary = this._pointers[0]; 83 - } 84 - 85 - public on(eventName: Events.up, handler: (event: PointerEvent) => void): void; 86 - public on(eventName: Events.down, handler: (event: PointerEvent) => void): void; 87 - public on(eventName: Events.move, handler: (event: PointerEvent) => void): void; 88 - public on(eventName: Events.enter, handler: (event: PointerEvent) => void): void; 89 - public on(eventName: Events.leave, handler: (event: PointerEvent) => void): void; 90 - public on(eventName: Events.cancel, handler: (event: PointerEvent) => void): void; 91 - public on(eventName: Events.wheel, handler: (event: WheelEvent) => void): void; 92 - public on(eventName: string, handler: (event: GameEvent<any>) => void): void; 93 - public on(eventName: string, handler: (event: any) => void): void { 94 - super.on(eventName, handler); 95 - } 96 - 97 - /** 98 - * Primary pointer (mouse, 1 finger, stylus, etc.) 99 - */ 100 - public primary: Pointer; 101 - 102 - /** 103 - * Initializes pointer event listeners 104 - */ 105 - public init(target?: GlobalEventHandlers): void { 106 - target = target || this._engine.canvas; 107 - 108 - // Touch Events 109 - target.addEventListener('touchstart', this._handleTouchEvent('down', this._pointerDown)); 110 - target.addEventListener('touchend', this._handleTouchEvent('up', this._pointerUp)); 111 - target.addEventListener('touchmove', this._handleTouchEvent('move', this._pointerMove)); 112 - target.addEventListener('touchcancel', this._handleTouchEvent('cancel', this._pointerCancel)); 113 - 114 - // W3C Pointer Events 115 - // Current: IE11, IE10 116 - if ((<any>window).PointerEvent) { 117 - // IE11 118 - this._engine.canvas.style.touchAction = 'none'; 119 - target.addEventListener('pointerdown', this._handlePointerEvent('down', this._pointerDown)); 120 - target.addEventListener('pointerup', this._handlePointerEvent('up', this._pointerUp)); 121 - target.addEventListener('pointermove', this._handlePointerEvent('move', this._pointerMove)); 122 - target.addEventListener('pointercancel', this._handlePointerEvent('cancel', this._pointerCancel)); 123 - } else if ((<any>window).MSPointerEvent) { 124 - // IE10 125 - (this._engine.canvas.style as any).msTouchAction = 'none'; 126 - target.addEventListener('MSPointerDown', this._handlePointerEvent('down', this._pointerDown)); 127 - target.addEventListener('MSPointerUp', this._handlePointerEvent('up', this._pointerUp)); 128 - target.addEventListener('MSPointerMove', this._handlePointerEvent('move', this._pointerMove)); 129 - target.addEventListener('MSPointerCancel', this._handlePointerEvent('cancel', this._pointerCancel)); 130 - } else { 131 - // Mouse Events 132 - target.addEventListener('mousedown', this._handleMouseEvent('down', this._pointerDown)); 133 - target.addEventListener('mouseup', this._handleMouseEvent('up', this._pointerUp)); 134 - target.addEventListener('mousemove', this._handleMouseEvent('move', this._pointerMove)); 135 - } 136 - 137 - // MDN MouseWheelEvent 138 - const wheelOptions = { 139 - passive: !( 140 - this._engine.pageScrollPreventionMode === ScrollPreventionMode.All || 141 - this._engine.pageScrollPreventionMode === ScrollPreventionMode.Canvas 142 - ) 143 - }; 144 - if ('onwheel' in document.createElement('div')) { 145 - // Modern Browsers 146 - target.addEventListener('wheel', this._handleWheelEvent('wheel', this._wheel), wheelOptions); 147 - } else if (document.onmousewheel !== undefined) { 148 - // Webkit and IE 149 - target.addEventListener('mousewheel', this._handleWheelEvent('wheel', this._wheel), wheelOptions); 150 - } else { 151 - // Remaining browser and older Firefox 152 - target.addEventListener('MozMousePixelScroll', this._handleWheelEvent('wheel', this._wheel), wheelOptions); 153 - } 154 - } 155 - 156 - /** 157 - * Synthesize a pointer event that looks like a real browser event to excalibur 158 - * @param eventName 159 - * @param pos 160 - */ 161 - public triggerEvent( 162 - eventName: 'up' | 'down' | 'move' | 'cancel' | 'wheel', 163 - pos: Vector | GlobalCoordinates, 164 - button: NativePointerButton = NativePointerButton.Left, 165 - pointerType: 'mouse' | 'touch' | 'pen' = 'mouse', 166 - pointerId: number = 0 167 - ): void { 168 - let x = 0; 169 - let y = 0; 170 - let coords: GlobalCoordinates; 171 - if (pos instanceof GlobalCoordinates) { 172 - x = pos.pagePos.x; 173 - y = pos.pagePos.y; 174 - coords = pos; 175 - } else { 176 - x = pos.x; 177 - y = pos.y; 178 - coords = new GlobalCoordinates(pos.clone(), pos.clone(), pos.clone()); 179 - } 180 - 181 - const eventish = { 182 - pageX: x, 183 - pageY: y, 184 - pointerId: pointerId, 185 - pointerType: pointerType, 186 - button: button, 187 - preventDefault: () => { 188 - /* do nothing */ 189 - } 190 - }; 191 - 192 - switch (eventName) { 193 - case 'move': 194 - this._handlePointerEvent(eventName, this._pointerMove, coords)(eventish as DOMPointerEvent); 195 - break; 196 - case 'down': 197 - this._handlePointerEvent(eventName, this._pointerDown, coords)(eventish as DOMPointerEvent); 198 - break; 199 - case 'up': 200 - this._handlePointerEvent(eventName, this._pointerUp, coords)(eventish as DOMPointerEvent); 201 - break; 202 - case 'cancel': 203 - this._handlePointerEvent(eventName, this._pointerCancel, coords)(eventish as DOMPointerEvent); 204 - break; 205 - } 206 - for (const actor of this._engine.currentScene.actors) { 207 - const capturePointer = actor.traits.filter((t) => t instanceof CapturePointer)[0]; 208 - if (capturePointer) { 209 - capturePointer.update(actor, this._engine, 1); 210 - } 211 - } 212 - this.dispatchPointerEvents(); 213 - this.update(); 214 - } 215 - 216 - /** 217 - * Update all pointer events and pointers, meant to be called at the end of frame 218 - */ 219 - public update(): void { 220 - this._pointerUp.length = 0; 221 - this._pointerDown.length = 0; 222 - this._pointerMove.length = 0; 223 - this._pointerCancel.length = 0; 224 - this._wheel.length = 0; 225 - 226 - for (let i = 0; i < this._pointers.length; i++) { 227 - this._pointers[i].update(); 228 - } 229 - } 230 - 231 - /** 232 - * Safely gets a Pointer at a specific index and initializes one if it doesn't yet exist 233 - * @param index The pointer index to retrieve 234 - */ 235 - public at(index: number): Pointer { 236 - if (index >= this._pointers.length) { 237 - // Ensure there is a pointer to retrieve 238 - for (let i = this._pointers.length - 1, max = index; i < max; i++) { 239 - this._pointers.push(new Pointer()); 240 - this._activePointers.push(-1); 241 - } 242 - } 243 - 244 - return this._pointers[index]; 245 - } 246 - 247 - /** 248 - * Get number of pointers being watched 249 - */ 250 - public count(): number { 251 - return this._pointers.length; 252 - } 253 - 254 - public checkAndUpdateActorUnderPointer(actor: Actor) { 255 - for (const pointer of this._pointers) { 256 - if (pointer.checkActorUnderPointer(actor)) { 257 - pointer.addActorUnderPointer(actor); 258 - } else { 259 - pointer.removeActorUnderPointer(actor); 260 - } 261 - } 262 - } 263 - 264 - private _dispatchWithBubble(events: PointerEvent[]) { 265 - for (const evt of events) { 266 - for (const actor of evt.pointer.getActorsForEvents()) { 267 - if (!evt.isCanceled()) { 268 - evt.propagate(actor); 269 - } 270 - if (!evt.bubbles || evt.isCanceled()) { 271 - // if the event stops bubbling part way stop processing 272 - break; 273 - } 274 - } 275 - } 276 - } 277 - 278 - private _dispatchPointerLeaveEvents() { 279 - const lastMoveEventPerPointerPerActor: { [pointerId: string]: PointerEvent } = {}; 280 - const pointerLeave: PointerLeaveEvent[] = []; 281 - 282 - for (const evt of this._pointerMove) { 283 - for (const actor of evt.pointer.getActorsForEvents()) { 284 - // If the actor was under the pointer last frame, but not this this frame, pointer left 285 - if ( 286 - !lastMoveEventPerPointerPerActor[evt.pointer.id + '+' + actor.id] && 287 - evt.pointer.wasActorUnderPointer(actor) && 288 - !evt.pointer.isActorAliveUnderPointer(actor) && 289 - !evt.isCanceled() 290 - ) { 291 - lastMoveEventPerPointerPerActor[evt.pointer.id + '+' + actor.id] = evt; 292 - const pe = createPointerEventByName( 293 - 'leave', 294 - new GlobalCoordinates(evt.worldPos, evt.pagePos, evt.screenPos), 295 - evt.pointer, 296 - evt.index, 297 - evt.pointerType, 298 - evt.button, 299 - evt.ev 300 - ); 301 - 302 - pe.propagate(actor); 303 - pointerLeave.push(<PointerLeaveEvent>pe); 304 - } 305 - } 306 - } 307 - return pointerLeave; 308 - } 309 - 310 - private _dispatchPointerEnterEvents() { 311 - const lastMoveEventPerPointer: { [pointerId: number]: PointerEvent } = {}; 312 - const pointerEnter: PointerEnterEvent[] = []; 313 - 314 - for (const evt of this._pointerMove) { 315 - for (const actor of evt.pointer.getActorsForEvents()) { 316 - // If the actor was not under the pointer last frame, but it is this frame, pointer entered 317 - if ( 318 - !lastMoveEventPerPointer[evt.pointer.id] && 319 - !evt.pointer.wasActorUnderPointer(actor) && 320 - evt.pointer.isActorAliveUnderPointer(actor) && 321 - !evt.isCanceled() 322 - ) { 323 - lastMoveEventPerPointer[evt.pointer.id] = evt; 324 - const pe = createPointerEventByName( 325 - 'enter', 326 - new GlobalCoordinates(evt.worldPos, evt.pagePos, evt.screenPos), 327 - evt.pointer, 328 - evt.index, 329 - evt.pointerType, 330 - evt.button, 331 - evt.ev 332 - ); 333 - pe.propagate(actor); 334 - pointerEnter.push(<PointerEnterEvent>pe); 335 - // if pointer is dragging set the drag target 336 - if (evt.pointer.isDragging) { 337 - evt.pointer.dragTarget = actor; 338 - } 339 - } 340 - } 341 - } 342 - return pointerEnter; 343 - } 344 - 345 - public dispatchPointerEvents() { 346 - this._dispatchWithBubble(this._pointerDown); 347 - this._dispatchWithBubble(this._pointerUp); 348 - this._dispatchWithBubble(this._pointerMove); 349 - this._dispatchPointerLeaveEvents(); 350 - this._dispatchPointerEnterEvents(); 351 - this._dispatchWithBubble(this._pointerCancel); 352 - 353 - // TODO some duplication here 354 - for (const evt of this._wheel) { 355 - for (const actor of this._pointers[evt.index].getActorsUnderPointer()) { 356 - this._propagateWheelPointerEvent(actor, evt); 357 - if (!evt.bubbles || evt.isCanceled()) { 358 - // if the event stops bubbling part way stop processing 359 - break; 360 - } 361 - } 362 - } 363 - } 364 - 365 - private _propagateWheelPointerEvent(actor: Actor, wheelEvent: WheelEvent) { 366 - actor.emit('pointerwheel', wheelEvent); 367 - 368 - // Recurse and propagate 369 - if (wheelEvent.bubbles && !wheelEvent.isCanceled() && actor.parent) { 370 - this._propagateWheelPointerEvent(actor.parent as Actor, wheelEvent); // TODO not true 371 - } 372 - } 373 - 374 - private _handleMouseEvent(eventName: string, eventArr: PointerEvent[]) { 375 - return (e: MouseEvent) => { 376 - e.preventDefault(); 377 - 378 - const pointer = this.at(0); 379 - const coordinates = GlobalCoordinates.fromPagePosition(e.pageX, e.pageY, this._engine); 380 - const pe = createPointerEventByName( 381 - eventName, 382 - coordinates, 383 - pointer, 384 - 0, 385 - PointerType.Mouse, 386 - this._nativeButtonToPointerButton(e.button), 387 - e 388 - ); 389 - 390 - eventArr.push(pe); 391 - pointer.eventDispatcher.emit(eventName, pe); 392 - }; 393 - } 394 - 395 - private _handleTouchEvent(eventName: string, eventArr: PointerEvent[]) { 396 - return (e: TouchEvent) => { 397 - e.preventDefault(); 398 - for (let i = 0, len = e.changedTouches.length; i < len; i++) { 399 - const index = this._pointers.length > 1 ? this._getPointerIndex(e.changedTouches[i].identifier) : 0; 400 - if (index === -1) { 401 - continue; 402 - } 403 - 404 - const pointer = this.at(index); 405 - const coordinates = GlobalCoordinates.fromPagePosition(e.changedTouches[i].pageX, e.changedTouches[i].pageY, this._engine); 406 - const pe = createPointerEventByName(eventName, coordinates, pointer, index, PointerType.Touch, PointerButton.Unknown, e); 407 - 408 - eventArr.push(pe); 409 - pointer.eventDispatcher.emit(eventName, pe); 410 - this.emit(eventName, pe); 411 - // only with multi-pointer 412 - if (this._pointers.length > 1) { 413 - if (eventName === 'up') { 414 - // remove pointer ID from pool when pointer is lifted 415 - this._activePointers[index] = -1; 416 - } else if (eventName === 'down') { 417 - // set pointer ID to given index 418 - this._activePointers[index] = e.changedTouches[i].identifier; 419 - } 420 - } 421 - } 422 - }; 423 - } 424 - 425 - private _handlePointerEvent(eventName: string, eventArr: PointerEvent[], coords?: GlobalCoordinates) { 426 - return (e: DOMPointerEvent) => { 427 - e.preventDefault(); 428 - 429 - // get the index for this pointer ID if multi-pointer is asked for 430 - const index = this._pointers.length > 1 ? this._getPointerIndex(e.pointerId) : 0; 431 - if (index === -1) { 432 - return; 433 - } 434 - 435 - const pointer = this.at(index); 436 - const coordinates = coords || GlobalCoordinates.fromPagePosition(e.pageX, e.pageY, this._engine); 437 - const pe = createPointerEventByName( 438 - eventName, 439 - coordinates, 440 - pointer, 441 - index, 442 - this._stringToPointerType(e.pointerType), 443 - this._nativeButtonToPointerButton(e.button), 444 - e 445 - ); 446 - 447 - eventArr.push(pe); 448 - pointer.eventDispatcher.emit(eventName, pe); 449 - this.emit(eventName, pe); 450 - 451 - // only with multi-pointer 452 - if (this._pointers.length > 1) { 453 - if (eventName === 'up') { 454 - // remove pointer ID from pool when pointer is lifted 455 - this._activePointers[index] = -1; 456 - } else if (eventName === 'down') { 457 - // set pointer ID to given index 458 - this._activePointers[index] = e.pointerId; 459 - } 460 - } 461 - }; 462 - } 463 - 464 - private _handleWheelEvent(eventName: string, eventArr: WheelEvent[]) { 465 - return (e: DOMWheelEvent) => { 466 - // Should we prevent page scroll because of this event 467 - if ( 468 - this._engine.pageScrollPreventionMode === ScrollPreventionMode.All || 469 - (this._engine.pageScrollPreventionMode === ScrollPreventionMode.Canvas && e.target === this._engine.canvas) 470 - ) { 471 - e.preventDefault(); 472 - } 473 - const screen = this._engine.screen.pageToScreenCoordinates(vec(e.pageX, e.pageY)); 474 - const world = this._engine.screen.screenToWorldCoordinates(screen); 475 - 476 - // deltaX, deltaY, and deltaZ are the standard modern properties 477 - // wheelDeltaX, wheelDeltaY, are legacy properties in webkit browsers and older IE 478 - // e.detail is only used in opera 479 - 480 - const deltaX = e.deltaX || e.wheelDeltaX * ScrollWheelNormalizationFactor || 0; 481 - const deltaY = 482 - e.deltaY || e.wheelDeltaY * ScrollWheelNormalizationFactor || e.wheelDelta * ScrollWheelNormalizationFactor || e.detail || 0; 483 - const deltaZ = e.deltaZ || 0; 484 - let deltaMode = WheelDeltaMode.Pixel; 485 - 486 - if (e.deltaMode) { 487 - if (e.deltaMode === 1) { 488 - deltaMode = WheelDeltaMode.Line; 489 - } else if (e.deltaMode === 2) { 490 - deltaMode = WheelDeltaMode.Page; 491 - } 492 - } 493 - 494 - const we = new WheelEvent(world.x, world.y, e.pageX, e.pageY, screen.x, screen.y, 0, deltaX, deltaY, deltaZ, deltaMode, e); 495 - 496 - eventArr.push(we); 497 - this.at(0).eventDispatcher.emit(eventName, we); 498 - this.emit(eventName, we); 499 - }; 500 - } 501 - 502 - /** 503 - * Gets the index of the pointer specified for the given pointer ID or finds the next empty pointer slot available. 504 - * This is required because IE10/11 uses incrementing pointer IDs so we need to store a mapping of ID => idx 505 - */ 506 - private _getPointerIndex(pointerId: number) { 507 - let idx; 508 - if ((idx = this._activePointers.indexOf(pointerId)) > -1) { 509 - return idx; 510 - } 511 - 512 - for (let i = 0; i < this._activePointers.length; i++) { 513 - if (this._activePointers[i] === -1) { 514 - return i; 515 - } 516 - } 517 - 518 - // ignore pointer because game isn't watching 519 - return -1; 520 - } 521 - 522 - private _nativeButtonToPointerButton(s: NativePointerButton): PointerButton { 523 - switch (s) { 524 - case NativePointerButton.NoButton: 525 - return PointerButton.NoButton; 526 - case NativePointerButton.Left: 527 - return PointerButton.Left; 528 - case NativePointerButton.Middle: 529 - return PointerButton.Middle; 530 - case NativePointerButton.Right: 531 - return PointerButton.Right; 532 - case NativePointerButton.Unknown: 533 - return PointerButton.Unknown; 534 - default: 535 - return Util.fail(s); 536 - } 537 - } 538 - 539 - private _stringToPointerType(s: string) { 540 - switch (s) { 541 - case 'touch': 542 - return PointerType.Touch; 543 - case 'mouse': 544 - return PointerType.Mouse; 545 - case 'pen': 546 - return PointerType.Pen; 547 - default: 548 - return PointerType.Unknown; 549 - } 550 - } 551 - }
+6
src/engine/Input/WheelDeltaMode.ts
··· 1 + 2 + export enum WheelDeltaMode { 3 + Pixel = 'Pixel', 4 + Line = 'Line', 5 + Page = 'Page' 6 + }
+23
src/engine/Input/WheelEvent.ts
··· 1 + import { WheelDeltaMode } from './WheelDeltaMode'; 2 + 3 + 4 + export class WheelEvent { 5 + public active = true; 6 + public cancel() { 7 + this.active = false; 8 + } 9 + constructor( 10 + public x: number, 11 + public y: number, 12 + public pageX: number, 13 + public pageY: number, 14 + public screenX: number, 15 + public screenY: number, 16 + public index: number, 17 + public deltaX: number, 18 + public deltaY: number, 19 + public deltaZ: number, 20 + public deltaMode: WheelDeltaMode, 21 + public ev: Event 22 + ) { } 23 + }
+17 -17
src/engine/Interfaces/PointerEventHandlers.ts
··· 1 1 import * as Events from '../Events'; 2 - import { PointerEvent, WheelEvent, PointerDragEvent } from '../Input/PointerEvents'; 3 - 2 + import { PointerEvent } from '../Input/PointerEvent'; 3 + import { WheelEvent } from '../Input/WheelEvent'; 4 4 export interface PointerEvents { 5 5 on(eventName: Events.pointerup, handler: (event: PointerEvent) => void): void; 6 6 on(eventName: Events.pointerdown, handler: (event: PointerEvent) => void): void; ··· 9 9 on(eventName: Events.pointermove, handler: (event: PointerEvent) => void): void; 10 10 on(eventName: Events.pointercancel, handler: (event: PointerEvent) => void): void; 11 11 on(eventName: Events.pointerwheel, handler: (event: WheelEvent) => void): void; 12 - on(eventName: Events.pointerdragstart, handler: (event: PointerDragEvent) => void): void; 13 - on(eventName: Events.pointerdragend, handler: (event: PointerDragEvent) => void): void; 14 - on(eventName: Events.pointerdragenter, handler: (event: PointerDragEvent) => void): void; 15 - on(eventName: Events.pointerdragleave, handler: (event: PointerDragEvent) => void): void; 16 - on(eventName: Events.pointerdragmove, handler: (event: PointerDragEvent) => void): void; 12 + on(eventName: Events.pointerdragstart, handler: (event: PointerEvent) => void): void; 13 + on(eventName: Events.pointerdragend, handler: (event: PointerEvent) => void): void; 14 + on(eventName: Events.pointerdragenter, handler: (event: PointerEvent) => void): void; 15 + on(eventName: Events.pointerdragleave, handler: (event: PointerEvent) => void): void; 16 + on(eventName: Events.pointerdragmove, handler: (event: PointerEvent) => void): void; 17 17 18 18 once(eventName: Events.pointerup, handler: (event: PointerEvent) => void): void; 19 19 once(eventName: Events.pointerdown, handler: (event: PointerEvent) => void): void; ··· 22 22 once(eventName: Events.pointermove, handler: (event: PointerEvent) => void): void; 23 23 once(eventName: Events.pointercancel, handler: (event: PointerEvent) => void): void; 24 24 once(eventName: Events.pointerwheel, handler: (event: WheelEvent) => void): void; 25 - once(eventName: Events.pointerdragstart, handler: (event: PointerDragEvent) => void): void; 26 - once(eventName: Events.pointerdragend, handler: (event: PointerDragEvent) => void): void; 27 - once(eventName: Events.pointerdragenter, handler: (event: PointerDragEvent) => void): void; 28 - once(eventName: Events.pointerdragleave, handler: (event: PointerDragEvent) => void): void; 29 - once(eventName: Events.pointerdragmove, handler: (event: PointerDragEvent) => void): void; 25 + once(eventName: Events.pointerdragstart, handler: (event: PointerEvent) => void): void; 26 + once(eventName: Events.pointerdragend, handler: (event: PointerEvent) => void): void; 27 + once(eventName: Events.pointerdragenter, handler: (event: PointerEvent) => void): void; 28 + once(eventName: Events.pointerdragleave, handler: (event: PointerEvent) => void): void; 29 + once(eventName: Events.pointerdragmove, handler: (event: PointerEvent) => void): void; 30 30 31 31 off(eventName: Events.pointerup, handler?: (event: PointerEvent) => void): void; 32 32 off(eventName: Events.pointerdown, handler?: (event: PointerEvent) => void): void; ··· 35 35 off(eventName: Events.pointermove, handler?: (event: PointerEvent) => void): void; 36 36 off(eventName: Events.pointercancel, handler?: (event: PointerEvent) => void): void; 37 37 off(eventName: Events.pointerwheel, handler?: (event: WheelEvent) => void): void; 38 - off(eventName: Events.pointerdragstart, handler?: (event: PointerDragEvent) => void): void; 39 - off(eventName: Events.pointerdragend, handler?: (event: PointerDragEvent) => void): void; 40 - off(eventName: Events.pointerdragenter, handler?: (event: PointerDragEvent) => void): void; 41 - off(eventName: Events.pointerdragleave, handler?: (event: PointerDragEvent) => void): void; 42 - off(eventName: Events.pointerdragmove, handler?: (event: PointerDragEvent) => void): void; 38 + off(eventName: Events.pointerdragstart, handler?: (event: PointerEvent) => void): void; 39 + off(eventName: Events.pointerdragend, handler?: (event: PointerEvent) => void): void; 40 + off(eventName: Events.pointerdragenter, handler?: (event: PointerEvent) => void): void; 41 + off(eventName: Events.pointerdragleave, handler?: (event: PointerEvent) => void): void; 42 + off(eventName: Events.pointerdragmove, handler?: (event: PointerEvent) => void): void; 43 43 }
+2 -3
src/engine/Scene.ts
··· 33 33 import { CanvasDrawingSystem } from './Drawing/CanvasDrawingSystem'; 34 34 import { Flags, Legacy } from './Flags'; 35 35 import { DebugSystem } from './Debug/DebugSystem'; 36 + import { PointerSystem } from './Input/PointerSystem'; 36 37 import { ActionsSystem } from './Actions/ActionsSystem'; 37 38 /** 38 39 * [[Actor|Actors]] are composed together into groupings called Scenes in ··· 110 111 111 112 constructor() { 112 113 super(); 113 - // TODO how to people do there own systems 114 114 // Initialize systems 115 115 this.world.add(new ActionsSystem()); 116 116 this.world.add(new MotionSystem()); 117 117 this.world.add(new CollisionSystem()); 118 + this.world.add(new PointerSystem()); 118 119 if (Flags.isEnabled(Legacy.LegacyDrawing)) { 119 120 this.world.add(new CanvasDrawingSystem()); 120 121 } else { ··· 359 360 this.world.update(SystemType.Update, delta); 360 361 361 362 this._collectActorStats(engine); 362 - 363 - engine.input.pointers.dispatchPointerEvents(); 364 363 365 364 this._postupdate(engine, delta); 366 365 }
-4
src/engine/ScreenElement.ts
··· 1 1 import { Vector, vec } from './Math/vector'; 2 2 import { Engine } from './Engine'; 3 3 import { Actor, ActorArgs } from './Actor'; 4 - import * as Traits from './Traits/Index'; 5 4 import { CoordPlane, TransformComponent } from './EntityComponentSystem/Components/TransformComponent'; 6 5 import { CollisionType } from './Collision/CollisionType'; 7 6 ··· 18 17 constructor(config?: ActorArgs) { 19 18 super({ ...config }); 20 19 this.get(TransformComponent).coordPlane = CoordPlane.Screen; 21 - this.traits = []; 22 - this.traits.push(new Traits.CapturePointer()); 23 20 this.anchor = vec(0, 0); 24 21 this.body.collisionType = CollisionType.PreventCollision; 25 22 this.collider.useBoxCollider(this.width, this.height, this.anchor); 26 - this.enableCapturePointer = true; 27 23 } 28 24 29 25 public _initialize(engine: Engine) {
-20
src/engine/Traits/CapturePointer.ts
··· 1 - import { Trait } from '../Interfaces/Trait'; 2 - import { Actor } from '../Actor'; 3 - import { Engine } from '../Engine'; 4 - 5 - /** 6 - * Revises pointer events path accordingly to the actor 7 - * @deprecated Will be removed in 0.26.0 8 - */ 9 - export class CapturePointer implements Trait { 10 - public update(actor: Actor, engine: Engine) { 11 - if (!actor.enableCapturePointer) { 12 - return; 13 - } 14 - if (actor.isKilled()) { 15 - return; 16 - } 17 - 18 - engine.input.pointers.checkAndUpdateActorUnderPointer(actor); 19 - } 20 - }
-1
src/engine/Traits/Index.ts
··· 1 1 /** 2 2 * @module 3 3 */ 4 - export * from './CapturePointer'; 5 4 export * from './OffscreenCulling';
+1
src/engine/index.ts
··· 55 55 // ex.Input namespace 56 56 import * as input from './Input/Index'; 57 57 export { input as Input }; 58 + export { PointerComponent } from './Input/Index'; 58 59 59 60 // ex.Traits namespace 60 61 import * as traits from './Traits/Index';
+13 -196
src/spec/ActorSpec.ts
··· 45 45 46 46 engine.start(); 47 47 collisionSystem.initialize(scene); 48 + scene.world.systemManager.get(ex.Input.PointerSystem).initialize(scene); 48 49 49 50 ex.Physics.useArcadePhysics(); 50 51 ex.Physics.acc.setTo(0, 0); ··· 548 549 expect(actor.draw).not.toHaveBeenCalled(); 549 550 }); 550 551 551 - it('does not incur pointer overhead until an event is registered', () => { 552 - expect(actor.enableCapturePointer).toBeFalsy(); 553 - expect(actor.capturePointer.captureMoveEvents).toBeFalsy(); 554 - actor.on('pointerdown', () => { 555 - /*do nothing*/ 556 - }); 557 - expect(actor.capturePointer.captureMoveEvents).toBeFalsy(); 558 - expect(actor.enableCapturePointer).toBeTruthy(); 559 - actor.on('pointermove', () => { 560 - /*do nothing*/ 561 - }); 562 - expect(actor.capturePointer.captureMoveEvents).toBeTruthy(); 563 - expect(actor.enableCapturePointer).toBeTruthy(); 564 - }); 565 - 566 552 it('does not change opacity on color', () => { 567 553 actor.color = ex.Color.Black.clone(); 568 554 expect(actor.color.a).toBe(1); ··· 1018 1004 scene.draw(engine.ctx, 100); 1019 1005 }); 1020 1006 1021 - it('should opt into pointer capture when pointerdown', () => { 1022 - const actor = new ex.Actor({ x: 0, y: 0, width: 100, height: 100 }); 1023 - 1024 - expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer capture enabled'); 1025 - expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not start with capturing move events'); 1026 - expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not start with capturing drag events'); 1027 - 1028 - actor.on('pointerdown', () => { 1029 - /* doesn't matter; */ 1030 - }); 1031 - 1032 - expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer capture enabled after pointerdown'); 1033 - expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not capture move events after pointerdown'); 1034 - expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not capture drag events after pointerdown'); 1035 - }); 1036 - 1037 - it('should opt into pointer capture when pointerup', () => { 1038 - const actor = new ex.Actor({ x: 0, y: 0, width: 100, height: 100 }); 1039 - 1040 - expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer capture enabled'); 1041 - expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not start with capturing move events'); 1042 - expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not start with capturing drag events'); 1043 - 1044 - actor.on('pointerup', () => { 1045 - /* doesn't matter; */ 1046 - }); 1047 - 1048 - expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer capture enabled after pointerup'); 1049 - expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not capture move events after pointerdown'); 1050 - expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not capture drag events after pointerdown'); 1051 - }); 1052 - 1053 - it('should opt into pointer capture when pointermove', () => { 1054 - const actor = new ex.Actor({ x: 0, y: 0, width: 100, height: 100 }); 1055 - 1056 - expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer capture enabled'); 1057 - expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not start with capturing move events'); 1058 - expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not start with capturing drag events'); 1059 - 1060 - actor.on('pointermove', () => { 1061 - /* doesn't matter; */ 1062 - }); 1063 - 1064 - expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer capture enabled after pointermove'); 1065 - expect(actor.capturePointer.captureMoveEvents).toBe(true, 'Actor should capture move events after pointermove'); 1066 - expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not capture drag events after pointermove'); 1067 - }); 1068 - 1069 - it('should opt into pointer capture when pointerenter', () => { 1070 - const actor = new ex.Actor({ x: 0, y: 0, width: 100, height: 100 }); 1071 - 1072 - expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer capture enabled'); 1073 - expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not start with capturing move events'); 1074 - expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not start with capturing drag events'); 1075 - 1076 - actor.on('pointerenter', () => { 1077 - /* doesn't matter; */ 1078 - }); 1079 - 1080 - expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer capture enabled after pointerenter'); 1081 - expect(actor.capturePointer.captureMoveEvents).toBe(true, 'Actor should capture move events after pointerenter'); 1082 - expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not capture drag events after pointerenter'); 1083 - }); 1084 - 1085 - it('should opt into pointer capture when pointerleave', () => { 1086 - const actor = new ex.Actor({ x: 0, y: 0, width: 100, height: 100 }); 1087 - 1088 - expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer capture enabled'); 1089 - expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not start with capturing move events'); 1090 - expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not start with capturing drag events'); 1091 - 1092 - actor.on('pointerleave', () => { 1093 - /* doesn't matter; */ 1094 - }); 1095 - 1096 - expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer capture enabled after pointerleave'); 1097 - expect(actor.capturePointer.captureMoveEvents).toBe(true, 'Actor should capture move events after pointerleave'); 1098 - expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not capture drag events after pointerleave'); 1099 - }); 1100 - 1101 - it('should opt into pointer capture when pointerdragstart', () => { 1102 - const actor = new ex.Actor({ x: 0, y: 0, width: 100, height: 100 }); 1103 - 1104 - expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer capture enabled'); 1105 - expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not start with capturing move events'); 1106 - expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not start with capturing drag events'); 1107 - 1108 - actor.on('pointerdragstart', () => { 1109 - /* doesn't matter; */ 1110 - }); 1111 - 1112 - expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer capture enabled after pointerdragstart'); 1113 - expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should capture move events after pointerdragstart'); 1114 - expect(actor.capturePointer.captureDragEvents).toBe(true, 'Actors should capture drag events after pointerdragstart'); 1115 - }); 1116 - 1117 - it('should opt into pointer capture when pointerdragend', () => { 1118 - const actor = new ex.Actor({ x: 0, y: 0, width: 100, height: 100 }); 1119 - 1120 - expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer capture enabled'); 1121 - expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not start with capturing move events'); 1122 - expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not start with capturing drag events'); 1123 - 1124 - actor.on('pointerdragend', () => { 1125 - /* doesn't matter; */ 1126 - }); 1127 - 1128 - expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer capture enabled after pointerdragend'); 1129 - expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should capture move events after pointerdragend'); 1130 - expect(actor.capturePointer.captureDragEvents).toBe(true, 'Actors should capture drag events after pointerdragend'); 1131 - }); 1132 - 1133 - it('should opt into pointer capture when pointerdragmove', () => { 1134 - const actor = new ex.Actor({ x: 0, y: 0, width: 100, height: 100 }); 1135 - 1136 - expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer capture enabled'); 1137 - expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not start with capturing move events'); 1138 - expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not start with capturing drag events'); 1139 - 1140 - actor.on('pointerdragmove', () => { 1141 - /* doesn't matter; */ 1142 - }); 1143 - 1144 - expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer capture enabled after pointerdragmove'); 1145 - expect(actor.capturePointer.captureMoveEvents).toBe(true, 'Actors should capture move events after pointerdragmove'); 1146 - expect(actor.capturePointer.captureDragEvents).toBe(true, 'Actors should capture drag events after pointerdragmove'); 1147 - }); 1148 - 1149 - it('should opt into pointer capture when pointerdragenter', () => { 1150 - const actor = new ex.Actor({ x: 0, y: 0, width: 100, height: 100 }); 1151 - 1152 - expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer capture enabled'); 1153 - expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not start with capturing move events'); 1154 - expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not start with capturing drag events'); 1155 - 1156 - actor.on('pointerdragenter', () => { 1157 - /* doesn't matter; */ 1158 - }); 1159 - 1160 - expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer capture enabled after pointerdragenter'); 1161 - expect(actor.capturePointer.captureMoveEvents).toBe(true, 'Actors should capture move events after pointerdragenter'); 1162 - expect(actor.capturePointer.captureDragEvents).toBe(true, 'Actors should capture drag events after pointerdragenter'); 1163 - }); 1164 - 1165 - it('should opt into pointer capture when pointerdragleave', () => { 1166 - const actor = new ex.Actor({ x: 0, y: 0, width: 100, height: 100 }); 1167 - 1168 - expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer capture enabled'); 1169 - expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not start with capturing move events'); 1170 - expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not start with capturing drag events'); 1171 - 1172 - actor.on('pointerdragleave', () => { 1173 - /* doesn't matter; */ 1174 - }); 1175 - 1176 - expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer capture enabled after pointerdragleave'); 1177 - expect(actor.capturePointer.captureMoveEvents).toBe(true, 'Actors should capture move events after pointerdragleave'); 1178 - expect(actor.capturePointer.captureDragEvents).toBe(true, 'Actors should capture drag events after pointerdragleave'); 1179 - }); 1180 - 1181 1007 describe('should detect assigned events and', () => { 1182 1008 it('should capture pointer move event', () => { 1183 1009 const actor = new ex.Actor({ x: 0, y: 0, width: 20, height: 20 }); ··· 1218 1044 1219 1045 it('should capture pointer leave event', () => { 1220 1046 const actor = new ex.Actor({ x: 0, y: 0, width: 20, height: 20 }); 1221 - const callables = { 1222 - leave: (pe: any) => { 1223 - /* doesn't matter */ 1224 - } 1225 - }; 1226 - const leaveSpy = spyOn(callables, 'leave').and.callThrough(); 1227 1047 1228 - actor.on('pointerleave', callables.leave); 1048 + const leaveSpy = jasmine.createSpy('leave'); 1049 + actor.on('pointerleave', leaveSpy); 1229 1050 scene.add(actor); 1230 1051 1231 1052 engine.input.pointers.triggerEvent('move', new ex.Vector(0, 0)); ··· 1295 1116 1296 1117 it('should capture pointer drag enter event', () => { 1297 1118 const actor = new ex.Actor({ x: 0, y: 0, width: 20, height: 20 }); 1298 - const callables = { 1299 - dragEnter: (pe: any) => { 1300 - /* doesn't matter */ 1301 - } 1302 - }; 1303 - const dragEnterSpy = spyOn(callables, 'dragEnter').and.callThrough(); 1119 + const dragEnterSpy = jasmine.createSpy('dragenter'); 1304 1120 1305 - actor.on('pointerdragenter', callables.dragEnter); 1121 + actor.on('pointerdragenter', dragEnterSpy); 1306 1122 scene.add(actor); 1307 1123 1308 1124 engine.input.pointers.triggerEvent('down', new ex.Vector(-20, -20)); 1309 1125 engine.input.pointers.triggerEvent('move', new ex.Vector(0, 0)); 1126 + 1127 + // Process pointer events 1128 + scene.update(engine, 0); 1310 1129 1311 1130 expect(dragEnterSpy).toHaveBeenCalledTimes(1); 1312 1131 expect(engine.input.pointers.at(0).lastWorldPos).toBeVector(new ex.Vector(0, 0)); ··· 1314 1133 1315 1134 it('should capture pointer drag leave event', () => { 1316 1135 const actor = new ex.Actor({ x: 0, y: 0, width: 20, height: 20 }); 1317 - const callables = { 1318 - dragLeave: (pe: any) => { 1319 - /* doesn't matter */ 1320 - } 1321 - }; 1322 - const dragLeaveSpy = spyOn(callables, 'dragLeave').and.callThrough(); 1136 + const dragLeaveSpy = jasmine.createSpy('dragLeave'); 1323 1137 1324 - actor.on('pointerdragleave', callables.dragLeave); 1138 + actor.on('pointerdragleave', dragLeaveSpy); 1325 1139 scene.add(actor); 1326 1140 1327 1141 engine.input.pointers.triggerEvent('down', new ex.Vector(0, 0)); 1328 1142 engine.input.pointers.triggerEvent('move', new ex.Vector(30, 30)); 1143 + 1144 + // Process pointer events 1145 + scene.update(engine, 0); 1329 1146 1330 1147 expect(dragLeaveSpy).toHaveBeenCalledTimes(1); 1331 1148 expect(engine.input.pointers.at(0).lastWorldPos).toBeVector(new ex.Vector(30, 30));
+40 -39
src/spec/PointerInputSpec.ts
··· 33 33 engine.stop(); 34 34 }); 35 35 36 - it('should exist', () => { 37 - expect(ex.Input.Pointers).toBeDefined(); 38 - expect(engine.input.pointers).toBeTruthy(); 39 - }); 40 - 41 - it('should have a new id', () => { 42 - const pointer1 = new ex.Input.Pointer(); 43 - const pointer2 = new ex.Input.Pointer(); 44 - expect(pointer1.id).not.toEqual(pointer2.id); 45 - }); 46 - 47 36 it('should detect pointer event', () => { 48 37 expect((<any>window).PointerEvent).toBeDefined(); 49 38 }); ··· 67 56 executeMouseEvent('pointerdown', <any>document, ex.Input.NativePointerButton.Left); 68 57 executeMouseEvent('pointerdown', <any>document, ex.Input.NativePointerButton.Right); 69 58 executeMouseEvent('pointerdown', <any>document, ex.Input.NativePointerButton.Middle); 59 + // process pointer events 60 + engine.currentScene.update(engine, 0); 70 61 71 62 expect(eventLeftFired).toBe(true, 'left should fire'); 72 63 expect(eventRightFired).toBe(true, 'right should fire'); ··· 93 84 executeMouseEvent('pointerup', <any>document, ex.Input.NativePointerButton.Left); 94 85 executeMouseEvent('pointerup', <any>document, ex.Input.NativePointerButton.Right); 95 86 executeMouseEvent('pointerup', <any>document, ex.Input.NativePointerButton.Middle); 87 + // process pointer events 88 + engine.currentScene.update(engine, 0); 96 89 97 90 expect(eventLeftFired).toBeTruthy('left should fire'); 98 91 expect(eventRightFired).toBeTruthy('right should fire'); ··· 107 100 }); 108 101 109 102 executeMouseEvent('pointermove', <any>document); 103 + // process pointer events 104 + engine.currentScene.update(engine, 0); 110 105 111 106 expect(eventMoveFired).toBeTruthy(); 112 107 }); 113 108 114 109 it('should update last position on down and move', () => { 115 110 executeMouseEvent('pointerdown', <any>document, null, 10, 800); 111 + // process pointer events 112 + engine.currentScene.update(engine, 0); 116 113 expect(engine.input.pointers.primary.lastPagePos.x).toBe(10); 117 114 expect(engine.input.pointers.primary.lastPagePos.y).toBe(800); 118 115 119 116 executeMouseEvent('pointermove', <any>document, null, 100, 200); 117 + // process pointer events 118 + engine.currentScene.update(engine, 0); 120 119 121 120 expect(engine.input.pointers.primary.lastPagePos.x).toBe(100); 122 121 expect(engine.input.pointers.primary.lastPagePos.y).toBe(200); 123 122 124 123 executeMouseEvent('pointermove', <any>document, null, 300, 400); 124 + // process pointer events 125 + engine.currentScene.update(engine, 0); 125 126 126 127 expect(engine.input.pointers.primary.lastPagePos.x).toBe(300); 127 128 expect(engine.input.pointers.primary.lastPagePos.y).toBe(400); 128 129 }); 129 130 130 - it('should not throw when checking if actors are under pointer if no pointer events have happened yet', () => { 131 - const actor = new ex.Actor({ pos: new ex.Vector(50, 50), width: 100, height: 100 }); 132 - expect(() => engine.input.pointers.primary.checkActorUnderPointer(actor)).not.toThrowError(); 133 - expect(engine.input.pointers.primary.checkActorUnderPointer(actor)).toBe(false); 134 - }); 135 - 136 - it('should return true when an actor is under the pointer', () => { 137 - const actor = new ex.Actor({ pos: new ex.Vector(50, 50), width: 100, height: 100 }); 138 - executeMouseEvent('pointerdown', <any>document, null, 50, 50); 139 - 140 - expect(engine.input.pointers.primary.checkActorUnderPointer(actor)).toBe(true); 141 - }); 142 - it('should only add actors under pointer that are in scene', () => { 143 - const actor = new ex.Actor({ pos: new ex.Vector(50, 50), width: 100, height: 100 }); 144 - executeMouseEvent('pointerdown', <any>document, null, 50, 50); 145 - expect(engine.input.pointers.primary.isActorAliveUnderPointer(actor)).toBe(false); 146 - }); 147 - 148 131 it('should dispatch events in z order high to low', () => { 149 132 const actualOrder = []; 150 133 ··· 169 152 actualOrder.push('actor4'); 170 153 }); 171 154 172 - engine.input.pointers.primary.addActorUnderPointer(actor1); 173 - engine.input.pointers.primary.addActorUnderPointer(actor2); 174 - engine.input.pointers.primary.addActorUnderPointer(actor3); 175 - engine.input.pointers.primary.addActorUnderPointer(actor4); 155 + engine.add(actor1); 156 + engine.add(actor2); 157 + engine.add(actor3); 158 + engine.add(actor4); 176 159 177 160 executeMouseEvent('pointerdown', <any>document, null, 50, 50); 178 161 179 - engine.input.pointers.dispatchPointerEvents(); 162 + // process pointer events 163 + engine.currentScene.update(engine, 0); 180 164 181 165 expect(actualOrder).toEqual(['actor4', 'actor3', 'actor2', 'actor1']); 182 166 }); ··· 206 190 actualOrder.push('actor4'); 207 191 }); 208 192 209 - engine.input.pointers.primary.addActorUnderPointer(actor1); 210 - engine.input.pointers.primary.addActorUnderPointer(actor2); 211 - engine.input.pointers.primary.addActorUnderPointer(actor3); 212 - engine.input.pointers.primary.addActorUnderPointer(actor4); 193 + engine.add(actor1); 194 + engine.add(actor2); 195 + engine.add(actor3); 196 + engine.add(actor4); 213 197 214 198 executeMouseEvent('pointerdown', <any>document, null, 50, 50); 215 199 216 - engine.input.pointers.dispatchPointerEvents(); 200 + // process pointer events 201 + engine.currentScene.update(engine, 0); 217 202 218 203 expect(actualOrder).toEqual(['actor4', 'actor3']); 219 204 }); ··· 224 209 engine.input.pointers.on('up', upHandler); 225 210 226 211 executeMouseEvent('pointerup', <any>document, null, 50, 50); 212 + 213 + // process pointer events 214 + engine.currentScene.update(engine, 0); 215 + 227 216 expect(upHandler).toHaveBeenCalledTimes(1); 228 217 }); 229 218 ··· 232 221 engine.input.pointers.on('down', downHandler); 233 222 234 223 executeMouseEvent('pointerdown', <any>document, null, 50, 50); 224 + 225 + // process pointer events 226 + engine.currentScene.update(engine, 0); 227 + 235 228 expect(downHandler).toHaveBeenCalledTimes(1); 236 229 }); 237 230 ··· 240 233 engine.input.pointers.on('move', moveHandler); 241 234 242 235 executeMouseEvent('pointermove', <any>document, null, 50, 50); 236 + 237 + // process pointer events 238 + engine.currentScene.update(engine, 0); 239 + 243 240 expect(moveHandler).toHaveBeenCalledTimes(1); 244 241 }); 245 242 ··· 248 245 engine.input.pointers.on('wheel', wheelHandler); 249 246 250 247 executeMouseEvent('wheel', <any>document, null, 50, 50); 248 + 249 + // process pointer events 250 + engine.currentScene.update(engine, 0); 251 + 251 252 expect(wheelHandler).toHaveBeenCalledTimes(1); 252 253 }); 253 254 });
+1 -34
src/spec/util/Mocks.ts
··· 142 142 } 143 143 }, 144 144 // eslint-disable-next-line 145 - pointers: new ex.Input.Pointers(<any>this), 145 + pointers: new ex.Input.PointerEventReceiver(window, null), 146 146 gamepads: { 147 147 update: function () { 148 148 /* do nothing */ ··· 278 278 }; 279 279 280 280 return mockWindow; 281 - } 282 - 283 - pointerEvent(eventName, pointer: ex.Input.Pointer = new ex.Input.Pointer()): ex.Input.PointerEvent { 284 - const coordinates = new ex.GlobalCoordinates(new ex.Vector(0, 0), new ex.Vector(0, 0), new ex.Vector(0, 0)); 285 - const pointerType = ex.Input.PointerType.Mouse; 286 - const pointerButton = ex.Input.PointerButton.Unknown; 287 - 288 - pointer.lastWorldPos = new ex.Vector(0, 0); 289 - 290 - let factory: ex.Input.PointerEventFactory<ex.Input.PointerEvent>; 291 - 292 - switch (eventName) { 293 - case 'up': 294 - factory = new ex.Input.PointerEventFactory(<any>ex.Input.PointerUpEvent); 295 - break; 296 - case 'down': 297 - factory = new ex.Input.PointerEventFactory(<any>ex.Input.PointerDownEvent); 298 - break; 299 - case 'move': 300 - factory = new ex.Input.PointerEventFactory(<any>ex.Input.PointerMoveEvent); 301 - break; 302 - case 'cancel': 303 - factory = new ex.Input.PointerEventFactory(<any>ex.Input.PointerCancelEvent); 304 - break; 305 - case 'enter': 306 - factory = new ex.Input.PointerEventFactory(<any>ex.Input.PointerEnterEvent); 307 - break; 308 - case 'leave': 309 - factory = new ex.Input.PointerEventFactory(<any>ex.Input.PointerLeaveEvent); 310 - break; 311 - } 312 - 313 - return factory.create(coordinates, pointer, 0, pointerType, pointerButton, {}); 314 281 } 315 282 } 316 283 }
+3 -7
src/stories/Audio.stories.ts
··· 45 45 startOrPauseBtn.graphics.add('pause', pauseSprite); 46 46 startOrPauseBtn.graphics.show('play'); 47 47 48 - startOrPauseBtn.enableCapturePointer = true; 49 48 startOrPauseBtn.on('pointerup', (evt) => { 50 49 if (!guitarLoopSound.isPlaying()) { 51 50 guitarLoopSound.play(); ··· 53 52 guitarLoopSound.pause(); 54 53 } 55 54 56 - evt.stopPropagation(); 55 + evt.cancel(); 57 56 }); 58 57 stopBtn.graphics.show(stopSprite); 59 - stopBtn.enableCapturePointer = true; 60 58 61 59 const playheadStartPos = playTimeline.collider.bounds.left; 62 60 const playheadEndPos = playTimeline.collider.bounds.right; ··· 103 101 guitarLoopSound.stop(); 104 102 playHead.pos.setTo(playheadStartPos, playHead.pos.y); 105 103 106 - evt.stopPropagation(); 104 + evt.cancel(); 107 105 }); 108 106 109 107 game.add(stopBtn); ··· 138 136 // playSprite.fill(Color.White); 139 137 140 138 startBtn.graphics.add(playSprite); 141 - startBtn.enableCapturePointer = true; 142 139 startBtn.on('pointerup', (evt) => { 143 140 playAction('Playing campfire and forest sounds '); 144 141 // playSprite.fill(Color.Green); ··· 154 151 guitarLoopSound.play(); 155 152 }, startGuitarAt); 156 153 157 - evt.stopPropagation(); 154 + evt.cancel(); 158 155 }); 159 156 160 157 game.add(startBtn); ··· 177 174 const playSprite = new Sprite({ image: playIconTx, destSize: { width: 32, height: 32 } }); 178 175 // playSprite.fill(Color.White); 179 176 startBtn.graphics.add(playSprite); 180 - startBtn.enableCapturePointer = true; 181 177 182 178 startBtn.on('pointerup', function () { 183 179 playAction('Playing guitar sound, volume will adjust in 2 seconds');
+78 -1
src/stories/Pointers.stories.ts
··· 56 56 } 57 57 }; 58 58 59 + export const configuringPointerTarget: Story = withEngine(async (game, { useCollider, useGraphics }) => { 60 + class TestBlock extends ex.Actor { 61 + 62 + public rectangle = new ex.Rectangle({ 63 + width: 50, 64 + height: 50, 65 + color: ex.Color.Blue 66 + }); 67 + constructor(x: number, y: number) { 68 + super({ 69 + pos: new ex.Vector(x, y), 70 + height: 100, 71 + width: 100 72 + }); 73 + 74 + this.graphics.add(this.rectangle); 75 + 76 + this.get(ex.PointerComponent).useColliderShape = useCollider; 77 + this.get(ex.PointerComponent).useGraphicsBounds = useGraphics; 78 + } 79 + 80 + onInitialize() { 81 + this.on(ex.EventTypes.PointerUp, () => { 82 + this.rectangle.color = ex.Color.Black; 83 + }); 84 + this.on(ex.EventTypes.PointerDown, () => { 85 + this.rectangle.color = ex.Color.Green; 86 + }); 87 + this.on(ex.EventTypes.PointerEnter, () => { 88 + this.rectangle.color = ex.Color.Yellow; 89 + }); 90 + this.on(ex.EventTypes.PointerLeave, () => { 91 + this.rectangle.color = ex.Color.Red; 92 + }); 93 + this.on(ex.EventTypes.PointerMove, () => { 94 + this.rectangle.color = ex.Color.Yellow; 95 + }); 96 + } 97 + } 98 + 99 + const block = new TestBlock(100, 100); 100 + 101 + game.add(block); 102 + 103 + game.toggleDebug(); 104 + game.debug.entity.showId = false; 105 + game.debug.graphics.showBounds = true; 106 + game.debug.collider.showGeometry = true; 107 + game.debug.collider.showBounds = false; 108 + 109 + await game.start(new ex.Loader()); 110 + }); 111 + 112 + configuringPointerTarget.story = { 113 + parameters: { 114 + notes: 'Pointers can be configured to use the collider geometry and/or the graphic bounds for triggering events' 115 + } 116 + } 117 + configuringPointerTarget.argTypes = { 118 + useCollider: { 119 + name: 'Use Collider geometry for pointers (default true)', 120 + control: { 121 + type: 'boolean' 122 + } 123 + }, 124 + useGraphics: { 125 + name: 'Use Graphics bounds for pointers (default false)', 126 + control: { 127 + type: 'boolean' 128 + } 129 + } 130 + }; 131 + configuringPointerTarget.args = { 132 + useCollider: true, 133 + useGraphics: false 134 + }; 135 + 59 136 export const dragEvents: Story = withEngine(async (game) => { 60 137 class TestBlock extends ex.Actor { 61 138 constructor(x: number, y: number) { ··· 76 153 }); 77 154 this.on(ex.EventTypes.PointerDragMove, (e) => { 78 155 this.color = ex.Color.Yellow; 79 - this.pos.setTo(e.pos.x, e.pos.y); 156 + this.pos.setTo(e.worldPos.x, e.worldPos.y); 80 157 }); 81 158 this.on(ex.EventTypes.PointerLeave, () => { 82 159 this.color = ex.Color.Red;