diff --git a/sandbox/tests/input/pointer.html b/sandbox/tests/input/pointer.html index 81ab98002..9383fa371 100644 --- a/sandbox/tests/input/pointer.html +++ b/sandbox/tests/input/pointer.html @@ -20,7 +20,7 @@
Number of Pointers
0
Pointer (Primary) Button
-
+
None
Last Pointer Page Pos (x,y)
Last Pointer Screen Pos (x,y)
diff --git a/sandbox/tests/input/pointer.ts b/sandbox/tests/input/pointer.ts index a2b9c69d4..8c10e438f 100644 --- a/sandbox/tests/input/pointer.ts +++ b/sandbox/tests/input/pointer.ts @@ -8,25 +8,17 @@ var game = new ex.Engine({ }); var box = new ex.Actor(200, 200, 100, 100, ex.Color.Red); var cursor = new ex.Actor(0, 0, 10, 10, ex.Color.Chartreuse); -var boxPointerDown = false; +var boxPointerDragging = false; var uiElement = new ex.UIActor(200, 0, 200, 200); -uiElement.color = ex.Color.Azure.clone(); +uiElement.color = ex.Color.Azure; uiElement.on('pointerdown', (p: ex.Input.PointerEvent) => { console.log(p); - uiElement.color = ex.Color.Red.clone(); + uiElement.color = ex.Color.Red; }); -// Enable pointer input for box -//box.enableCapturePointer = true; - -// Enable tracking mouse movement for box -//box.capturePointer.captureMoveEvents = true; - // Change color of box when clicked -box.on("pointerup", (pe: ex.Input.PointerEvent) => { - boxPointerDown = false; - +box.on("pointerdown", (pe: ex.Input.PointerEvent) => { if (box.color == ex.Color.Red) { box.color = ex.Color.Blue; } else { @@ -34,17 +26,28 @@ box.on("pointerup", (pe: ex.Input.PointerEvent) => { } }); +// Set drag flag +box.on("pointerdragstart", (pe: ex.Input.PointerEvent) => { + boxPointerDragging = true; +}); + +// Set drag flag +box.on("pointerdragend", (pe: ex.Input.PointerEvent) => { + boxPointerDragging = false; +}); + // Drag box around -box.on("pointermove", (pe: ex.Input.PointerEvent) => { - if (boxPointerDown) { - box.pos.x = pe.x; - box.pos.y = pe.y; +box.on("pointerdragmove", (pe: ex.Input.PointerEvent) => { + if (boxPointerDragging) { + box.pos = pe.pointer.lastWorldPos; } }); -// Set pointer down flag -box.on("pointerdown", (pe: ex.Input.PointerEvent) => { - boxPointerDown = true; +// Drag box around +box.on("pointerdragleave", (pe: ex.Input.PointerEvent) => { + if (boxPointerDragging) { + box.pos = pe.pointer.lastWorldPos; + } }); box.on("pointerwheel", (pe: ex.Input.WheelEvent) => { @@ -53,8 +56,7 @@ box.on("pointerwheel", (pe: ex.Input.WheelEvent) => { // Follow cursor game.input.pointers.primary.on("move", (pe: ex.Input.PointerEvent) => { - cursor.pos.x = pe.x; - cursor.pos.y = pe.y; + cursor.pos = pe.pointer.lastWorldPos; }); // Button type @@ -62,7 +64,7 @@ game.input.pointers.primary.on("down", (pe: ex.Input.PointerEvent) => { document.getElementById("pointer-btn").innerHTML = ex.Input.PointerButton[pe.button]; }); game.input.pointers.primary.on("up", (pe: ex.Input.PointerEvent) => { - document.getElementById("pointer-btn").innerHTML = ""; + document.getElementById("pointer-btn").innerHTML = "None"; }); // Wheel diff --git a/src/engine/Actor.ts b/src/engine/Actor.ts index 3ef07e214..4095616a4 100644 --- a/src/engine/Actor.ts +++ b/src/engine/Actor.ts @@ -4,9 +4,10 @@ import { BoundingBox } from './Collision/BoundingBox'; import { Texture } from './Resources/Texture'; import { InitializeEvent, KillEvent, PreUpdateEvent, PostUpdateEvent, - PreDrawEvent, PostDrawEvent, PreDebugDrawEvent, PostDebugDrawEvent, + PreDrawEvent, PostDrawEvent, PreDebugDrawEvent, PostDebugDrawEvent, GameEvent, PostCollisionEvent, PreCollisionEvent, CollisionStartEvent, CollisionEndEvent } from './Events'; +import { PointerEvent, PointerDragEvent } from './Input/Pointer'; import { Engine } from './Engine'; import { Color } from './Drawing/Color'; import { Sprite } from './Drawing/Sprite'; @@ -382,13 +383,13 @@ export class ActorImpl extends Class implements IActionable, IEvented { * * The default is `null` which prevents a rectangle from being drawn. */ - public get color() : Color { + public get color(): Color { return this._color; } - public set color(v : Color) { + public set color(v: Color) { this._color = v.clone(); } - private _color : Color; + private _color: Color; /** * Whether or not to enable the [[CapturePointer]] trait that propagates @@ -400,7 +401,8 @@ export class ActorImpl extends Class implements IActionable, IEvented { * Configuration for [[CapturePointer]] trait */ public capturePointer: Traits.ICapturePointerConfig = { - captureMoveEvents: false + captureMoveEvents: false, + captureDragEvents: false }; private _zIndex: number = 0; @@ -435,7 +437,7 @@ export class ActorImpl extends Class implements IActionable, IEvented { // set default opacity of an actor to the color this.opacity = color.a; } - + // Build default pipeline //this.traits.push(new ex.Traits.EulerMovement()); // TODO: TileMaps should be converted to a collision area @@ -484,14 +486,34 @@ export class ActorImpl extends Class implements IActionable, IEvented { } } + private _capturePointerEvents: string[] = [ + 'pointerup', 'pointerdown', 'pointermove', 'pointerenter', 'pointerleave', + 'pointerdragstart', 'pointerdragend', 'pointerdragmove', 'pointerdragenter', 'pointerdragleave' + ]; + + private _captureMoveEvents: string[] = [ + 'pointermove', 'pointerenter', 'pointerleave', + 'pointerdragmove', 'pointerdragenter', 'pointerdragleave' + ]; + + private _captureDragEvents: string[] = [ + 'pointerdragstart', 'pointerdragend', 'pointerdragmove', 'pointerdragenter', 'pointerdragleave' + ]; + private _checkForPointerOptIn(eventName: string) { if (eventName) { const normalized = eventName.toLowerCase(); - if (normalized === 'pointerup' || normalized === 'pointerdown' || normalized === 'pointermove') { + + if (this._capturePointerEvents.indexOf(normalized) !== -1) { this.enableCapturePointer = true; - if (normalized === 'pointermove') { + + if (this._captureMoveEvents.indexOf(normalized) !== -1) { this.capturePointer.captureMoveEvents = true; } + + if (this._captureDragEvents.indexOf(normalized) !== -1) { + this.capturePointer.captureDragEvents = true; + } } } } @@ -510,9 +532,16 @@ export class ActorImpl extends Class implements IActionable, IEvented { public on(eventName: Events.postdebugdraw, handler: (event?: PostDebugDrawEvent) => void): void; public on(eventName: Events.pointerup, handler: (event?: PointerEvent) => void): void; public on(eventName: Events.pointerdown, handler: (event?: PointerEvent) => void): void; + public on(eventName: Events.pointerenter, handler: (event?: PointerEvent) => void): void; + public on(eventName: Events.pointerleave, handler: (event?: PointerEvent) => void): void; public on(eventName: Events.pointermove, handler: (event?: PointerEvent) => void): void; public on(eventName: Events.pointercancel, handler: (event?: PointerEvent) => void): void; public on(eventName: Events.pointerwheel, handler: (event?: WheelEvent) => void): void; + public on(eventName: Events.pointerdragstart, handler: (event?: PointerDragEvent) => void): void; + public on(eventName: Events.pointerdragend, handler: (event?: PointerDragEvent) => void): void; + public on(eventName: Events.pointerdragenter, handler: (event?: PointerDragEvent) => void): void; + public on(eventName: Events.pointerdragleave, handler: (event?: PointerDragEvent) => void): void; + public on(eventName: Events.pointerdragmove, handler: (event?: PointerDragEvent) => void): void; public on(eventName: string, handler: (event?: GameEvent) => void): void; public on(eventName: string, handler: (event?: any) => void): void { this._checkForPointerOptIn(eventName); @@ -533,9 +562,16 @@ export class ActorImpl extends Class implements IActionable, IEvented { public once(eventName: Events.postdebugdraw, handler: (event?: PostDebugDrawEvent) => void): void; public once(eventName: Events.pointerup, handler: (event?: PointerEvent) => void): void; public once(eventName: Events.pointerdown, handler: (event?: PointerEvent) => void): void; + public once(eventName: Events.pointerenter, handler: (event?: PointerEvent) => void): void; + public once(eventName: Events.pointerleave, handler: (event?: PointerEvent) => void): void; public once(eventName: Events.pointermove, handler: (event?: PointerEvent) => void): void; public once(eventName: Events.pointercancel, handler: (event?: PointerEvent) => void): void; public once(eventName: Events.pointerwheel, handler: (event?: WheelEvent) => void): void; + public once(eventName: Events.pointerdragstart, handler: (event?: PointerDragEvent) => void): void; + public once(eventName: Events.pointerdragend, handler: (event?: PointerDragEvent) => void): void; + public once(eventName: Events.pointerdragenter, handler: (event?: PointerDragEvent) => void): void; + public once(eventName: Events.pointerdragleave, handler: (event?: PointerDragEvent) => void): void; + public once(eventName: Events.pointerdragmove, handler: (event?: PointerDragEvent) => void): void; public once(eventName: string, handler: (event?: GameEvent) => void): void; public once(eventName: string, handler: (event?: any) => void): void { this._checkForPointerOptIn(eventName); @@ -825,7 +861,7 @@ export class ActorImpl extends Class implements IActionable, IEvented { /** * Returns the actor's [[BoundingBox]] calculated for this instant in world space. */ - public getBounds() { + public getBounds(): BoundingBox { // todo cache bounding box var anchor = this._getCalculatedAnchor(); var pos = this.getWorldPos(); @@ -1024,7 +1060,7 @@ export class ActorImpl extends Class implements IActionable, IEvented { } // Update child actors - for (var i = 0; i < this.children.length; i++) { + for (var i = 0; i < this.children.length; i++) { this.children[i].update(engine, delta); } diff --git a/src/engine/Algebra.ts b/src/engine/Algebra.ts index 9541b7f00..66b286e69 100644 --- a/src/engine/Algebra.ts +++ b/src/engine/Algebra.ts @@ -1,3 +1,6 @@ +import { Engine } from 'Engine'; +import * as Util from 'Util/Util'; + /** * A 2D vector on a plane. */ @@ -415,7 +418,7 @@ export class Line { } else if (y !== null) { return new Vector((y - b) / m, y); } else { - throw new Error('You must provide an X or a Y value'); + throw new Error('You must provide an X or a Y value'); } } @@ -496,4 +499,36 @@ export class Projection { } return 0; } +} + +export class GlobalCoordinates { + public static fromPagePosition(x: number, y: number, engine: Engine): GlobalCoordinates; + public static fromPagePosition(pos: Vector, engine: Engine): GlobalCoordinates; + public static fromPagePosition(xOrPos: number | Vector, yOrEngine: number | Engine, engineOrUndefined?: Engine): GlobalCoordinates { + var pageX: number; + var pageY: number; + var pagePos: Vector; + var engine: Engine; + + if (arguments.length === 3) { + pageX = xOrPos; + pageY = yOrEngine; + pagePos = new Vector(pageX, pageY); + engine = engineOrUndefined; + } else { + pagePos = xOrPos; + pageX = pagePos.x; + pageY = pagePos.y; + engine = yOrEngine; + } + + var screenX: number = pageX - Util.getPosition(engine.canvas).x; + var screenY: number = pageY - Util.getPosition(engine.canvas).y; + var screenPos = new Vector(screenX, screenY); + var worldPos = engine.screenToWorldCoordinates(screenPos); + + return new GlobalCoordinates(worldPos, pagePos, screenPos); + } + + constructor(public worldPos: Vector, public pagePos: Vector, public screenPos: Vector) { } } \ No newline at end of file diff --git a/src/engine/Events.ts b/src/engine/Events.ts index b82e3b1ec..876c6ddca 100644 --- a/src/engine/Events.ts +++ b/src/engine/Events.ts @@ -56,12 +56,16 @@ export type stop = 'stop'; export type pointerup = 'pointerup'; export type pointerdown = 'pointerdown'; export type pointermove = 'pointermove'; +export type pointerenter = 'pointerenter'; +export type pointerleave = 'pointerleave'; export type pointercancel = 'pointercancel'; export type pointerwheel = 'pointerwheel'; export type up = 'up'; export type down = 'down'; export type move = 'move'; +export type enter = 'enter'; +export type leave = 'leave'; export type cancel = 'cancel'; export type wheel = 'wheel'; @@ -69,6 +73,12 @@ export type press = 'press'; export type release = 'release'; export type hold = 'hold'; +export type pointerdragstart = 'pointerdragstart'; +export type pointerdragend = 'pointerdragend'; +export type pointerdragenter = 'pointerdragenter'; +export type pointerdragleave = 'pointerdragleave'; +export type pointerdragmove = 'pointerdragmove'; + /** * Base event type in Excalibur that all other event types derive from. Not all event types are thrown on all Excalibur game objects, * some events are unique to a type, others are not. @@ -276,7 +286,7 @@ export class HiddenEvent extends GameEvent { * Event thrown on an [[Actor|actor]] when a collision will occur this frame if it resolves */ export class PreCollisionEvent extends GameEvent { - + /** * @param actor The actor the event was thrown on * @param other The actor that will collided with the current actor @@ -389,7 +399,6 @@ export class EnterViewPortEvent extends GameEvent { } } - export class EnterTriggerEvent extends GameEvent { constructor(public target: Trigger, public actor: Actor) { super(); diff --git a/src/engine/Input/Pointer.ts b/src/engine/Input/Pointer.ts index dbe7a7b4d..8daf3e61f 100644 --- a/src/engine/Input/Pointer.ts +++ b/src/engine/Input/Pointer.ts @@ -1,8 +1,9 @@ -import {Engine, ScrollPreventionMode} from './../Engine'; +import { Engine, ScrollPreventionMode } from './../Engine'; import { GameEvent } from '../Events'; -import { UIActor } from '../UIActor'; -import { Vector } from '../Algebra'; +import { Actor } from '../Actor'; +import { Vector, GlobalCoordinates } from '../Algebra'; import { Class } from '../Class'; +import * as Actors from '../Util/Actors'; import * as Util from '../Util/Util'; import * as Events from '../Events'; @@ -66,35 +67,86 @@ const ScrollWheelNormalizationFactor = -1 / 40; */ export class PointerEvent extends GameEvent { + /** Deprecated. Use [[PointerEvent]].worldPos.x instead. */ + public get x(): number { + return this.coordinates.worldPos.x; + } + + /** Deprecated. Use [[PointerEvent]].worldPos.y instead. */ + public get y(): number { + return this.coordinates.worldPos.y; + } + + /** Deprecated. Use [[PointerEvent]].worldPos.x instead. */ + public get worldX(): number { + return this.coordinates.worldPos.x; + } + + /** Deprecated. Use [[PointerEvent]].worldPos.y instead. */ + public get worldY(): number { + return this.coordinates.worldPos.y; + } + + /** Deprecated. Use [[PointerEvent]].pagePos.x instead. */ + public get pageX(): number { + return this.coordinates.pagePos.x; + } + + /** Deprecated. Use [[PointerEvent]].pagePos.y instead. */ + public get pageY(): number { + return this.coordinates.pagePos.y; + } + + /** Deprecated. Use [[PointerEvent]].screenPos.x instead. */ + public get screenX(): number { + return this.coordinates.screenPos.x; + } + + /** Deprecated. Use [[PointerEvent]].screenPos.y instead. */ + public get screenY(): number { + return this.coordinates.screenPos.y; + } + + /** The world coordinates of the event. */ + public get worldPos(): Vector { + return this.coordinates.worldPos.clone(); + } + + /** The page coordinates of the event. */ + public get pagePos(): Vector { + return this.coordinates.pagePos.clone(); + } + + /** The screen coordinates of the event. */ + public get screenPos(): Vector { + return this.coordinates.screenPos.clone(); + } + /** - * @param pageX The `x` coordinate of the event (in document coordinates) - * @param pageY The `y` coordinate of the event (in document coordinates) - * @param screenX The `x` coordinate of the event (in screen coordinates) - * @param screenY The `y` coordinate of the event (in screen coordinates) - * @param index The index of the pointer (zero-based) - * @param pointerType The type of pointer - * @param button The button pressed (if [[PointerType.Mouse]]) - * @param ev The raw DOM event being handled - * @param pos (Will be added to signature in 0.14.0 release) The position of the event (in world coordinates) + * @param coordinates The [[GlobalCoordinates]] of the event + * @param pointer The [[Pointer]] of the event + * @param index The index of the pointer (zero-based) + * @param pointerType The type of pointer + * @param button The button pressed (if [[PointerType.Mouse]]) + * @param ev The raw DOM event being handled + * @param pos (Will be added to signature in 0.14.0 release) The position of the event (in world coordinates) */ - constructor(public x: number, - public y: number, - public pageX: number, - public pageY: number, - public screenX: number, - public screenY: number, - public index: number, - public pointerType: PointerType, - public button: PointerButton, - public ev: any) { + constructor(private coordinates: GlobalCoordinates, + public pointer: Pointer, + public index: number, + public pointerType: PointerType, + public button: PointerButton, + public ev: any) { super(); } public get pos(): Vector { - return new Vector(this.x, this.y); + return this.coordinates.worldPos.clone(); } }; +export class PointerDragEvent extends PointerEvent { } + /** * Wheel Events * @@ -118,17 +170,17 @@ export class WheelEvent extends GameEvent { * @param ev The raw DOM event being handled */ constructor(public x: number, - public y: number, - public pageX: number, - public pageY: number, - public screenX: number, - public screenY: number, - public index: number, - public deltaX: number, - public deltaY: number, - public deltaZ: number, - public deltaMode: WheelDeltaMode, - public ev: any) { + public y: number, + public pageX: number, + public pageY: number, + public screenX: number, + public screenY: number, + public index: number, + public deltaX: number, + public deltaY: number, + public deltaZ: number, + public deltaMode: WheelDeltaMode, + public ev: any) { super(); } }; @@ -162,6 +214,8 @@ export class Pointers extends Class { public on(eventName: Events.up, handler: (event?: PointerEvent) => void): void; public on(eventName: Events.down, handler: (event?: PointerEvent) => void): void; public on(eventName: Events.move, handler: (event?: PointerEvent) => void): void; + public on(eventName: Events.enter, handler: (event?: PointerEvent) => void): void; + public on(eventName: Events.leave, handler: (event?: PointerEvent) => void): void; public on(eventName: Events.cancel, handler: (event?: PointerEvent) => void): void; public on(eventName: Events.wheel, handler: (event?: WheelEvent) => void): void; public on(eventName: string, handler: (event?: GameEvent) => void): void; @@ -194,18 +248,15 @@ export class Pointers extends Class { target.addEventListener('pointerdown', this._handlePointerEvent('down', this._pointerDown)); target.addEventListener('pointerup', this._handlePointerEvent('up', this._pointerUp)); target.addEventListener('pointermove', this._handlePointerEvent('move', this._pointerMove)); - target.addEventListener('pointercancel', this._handlePointerEvent('cancel', this._pointerMove)); - + target.addEventListener('pointercancel', this._handlePointerEvent('cancel', this._pointerCancel)); } else if ((window).MSPointerEvent) { // IE10 this._engine.canvas.style.msTouchAction = 'none'; target.addEventListener('MSPointerDown', this._handlePointerEvent('down', this._pointerDown)); target.addEventListener('MSPointerUp', this._handlePointerEvent('up', this._pointerUp)); target.addEventListener('MSPointerMove', this._handlePointerEvent('move', this._pointerMove)); - target.addEventListener('MSPointerCancel', this._handlePointerEvent('cancel', this._pointerMove)); - + target.addEventListener('MSPointerCancel', this._handlePointerEvent('cancel', this._pointerCancel)); } else { - // Mouse Events target.addEventListener('mousedown', this._handleMouseEvent('down', this._pointerDown)); target.addEventListener('mouseup', this._handleMouseEvent('up', this._pointerUp)); @@ -231,6 +282,10 @@ export class Pointers extends Class { this._pointerMove.length = 0; this._pointerCancel.length = 0; this._wheel.length = 0; + + for (let i = 0; i < this._pointers.length; i++) { + this._pointers[i].update(); + } } /** @@ -260,34 +315,81 @@ export class Pointers extends Class { /** * Propogates events to actor if necessary */ - public propogate(actor: any) { - var isUIActor = actor instanceof UIActor; - var i: number = 0, - len: number = this._pointerUp.length; + public propogate(actor: Actor) { + var isNotUIActor = !Actors.isUIActor(actor); + var i: number = 0; + var len: number = 0; + var pointerEvent: PointerEvent; + var pointer: Pointer; + + i = 0; + len = this._pointerDown.length; for (i; i < len; i++) { - if (actor.contains(this._pointerUp[i].x, this._pointerUp[i].y, !isUIActor)) { - actor.eventDispatcher.emit('pointerup', this._pointerUp[i]); + pointerEvent = this._pointerDown[i]; + pointer = pointerEvent.pointer; + + if (actor.contains(pointer.lastWorldPos.x, pointer.lastWorldPos.y, isNotUIActor)) { + actor.eventDispatcher.emit('pointerdown', pointerEvent); + + if (pointer.isDragStart && actor.capturePointer.captureDragEvents) { + actor.eventDispatcher.emit('pointerdragstart', pointerEvent); + } } } i = 0; - len = this._pointerDown.length; + len = this._pointerUp.length; for (i; i < len; i++) { - if (actor.contains(this._pointerDown[i].x, this._pointerDown[i].y, !isUIActor)) { - actor.eventDispatcher.emit('pointerdown', this._pointerDown[i]); + pointerEvent = this._pointerUp[i]; + pointer = pointerEvent.pointer; + + if (actor.contains(pointer.lastWorldPos.x, pointer.lastWorldPos.y, isNotUIActor)) { + actor.eventDispatcher.emit('pointerup', pointerEvent); + + if (pointer.isDragEnd && actor.capturePointer.captureDragEvents) { + actor.eventDispatcher.emit('pointerdragend', pointerEvent); + } } } if (actor.capturePointer.captureMoveEvents) { - i = 0; len = this._pointerMove.length; for (i; i < len; i++) { - if (actor.contains(this._pointerMove[i].x, this._pointerMove[i].y, !isUIActor)) { - actor.eventDispatcher.emit('pointermove', this._pointerMove[i]); + pointerEvent = this._pointerMove[i]; + pointer = pointerEvent.pointer; + + if (actor.contains(pointer.lastWorldPos.x, pointer.lastWorldPos.y, isNotUIActor)) { + if (!pointer.actorsUnderPointer[actor.id]) { + pointer.actorsUnderPointer[actor.id] = actor; + + actor.eventDispatcher.emit('pointerenter', pointerEvent); + + if (pointer.isDragging && actor.capturePointer.captureDragEvents) { + actor.eventDispatcher.emit('pointerdragenter', pointerEvent); + } + } + + actor.eventDispatcher.emit('pointermove', pointerEvent); + + if (pointer.isDragging && actor.capturePointer.captureDragEvents) { + actor.eventDispatcher.emit('pointerdragmove', pointerEvent); + } + } + + if (!actor.contains(pointer.lastWorldPos.x, pointer.lastWorldPos.y, isNotUIActor)) { + if (pointer.actorsUnderPointer[actor.id]) { + delete pointer.actorsUnderPointer[actor.id]; + + actor.eventDispatcher.emit('pointerleave', pointerEvent); + + if (pointer.isDragging && actor.capturePointer.captureDragEvents) { + actor.eventDispatcher.emit('pointerdragleave', pointerEvent); + } + } } } } @@ -296,15 +398,19 @@ export class Pointers extends Class { len = this._pointerCancel.length; for (i; i < len; i++) { - if (actor.contains(this._pointerCancel[i].x, this._pointerCancel[i].y, !isUIActor)) { - actor.eventDispatcher.emit('pointercancel', this._pointerCancel[i]); + pointerEvent = this._pointerCancel[i]; + pointer = pointerEvent.pointer; + + if (actor.contains(pointer.lastWorldPos.x, pointer.lastWorldPos.y, isNotUIActor)) { + actor.eventDispatcher.emit('pointercancel', pointerEvent); } } i = 0; len = this._wheel.length; + for (i; i < len; i++) { - if (actor.contains(this._wheel[i].x, this._wheel[i].y, !isUIActor)) { + if (actor.contains(this._wheel[i].x, this._wheel[i].y, isNotUIActor)) { actor.eventDispatcher.emit('pointerwheel', this._wheel[i]); } } @@ -313,12 +419,13 @@ export class Pointers extends Class { private _handleMouseEvent(eventName: string, eventArr: PointerEvent[]) { return (e: MouseEvent) => { e.preventDefault(); - var x: number = e.pageX - Util.getPosition(this._engine.canvas).x; - var y: number = e.pageY - Util.getPosition(this._engine.canvas).y; - var transformedPoint = this._engine.screenToWorldCoordinates(new Vector(x, y)); - var pe = new PointerEvent(transformedPoint.x, transformedPoint.y, e.pageX, e.pageY, x, y, 0, PointerType.Mouse, e.button, e); + + var pointer = this.at(0); + var coordinates = GlobalCoordinates.fromPagePosition(e.pageX, e.pageY, this._engine); + var pe = new PointerEvent(coordinates, pointer, 0, PointerType.Mouse, e.button, e); + eventArr.push(pe); - this.at(0).eventDispatcher.emit(eventName, pe); + pointer.eventDispatcher.emit(eventName, pe); }; } @@ -328,13 +435,14 @@ export class Pointers extends Class { for (var i = 0, len = e.changedTouches.length; i < len; i++) { var index = this._pointers.length > 1 ? this._getPointerIndex(e.changedTouches[i].identifier) : 0; if (index === -1) { continue; } - var x: number = e.changedTouches[i].pageX - Util.getPosition(this._engine.canvas).x; - var y: number = e.changedTouches[i].pageY - Util.getPosition(this._engine.canvas).y; - var transformedPoint = this._engine.screenToWorldCoordinates(new Vector(x, y)); - var pe = new PointerEvent(transformedPoint.x, transformedPoint.y, - e.changedTouches[i].pageX, e.changedTouches[i].pageY, x, y, index, PointerType.Touch, PointerButton.Unknown, e); + + var pointer = this.at(index); + var coordinates = GlobalCoordinates.fromPagePosition( + e.changedTouches[i].pageX, e.changedTouches[i].pageY, this._engine); + var pe = new PointerEvent(coordinates, pointer, index, PointerType.Touch, PointerButton.Unknown, e); + eventArr.push(pe); - this.at(index).eventDispatcher.emit(eventName, pe); + pointer.eventDispatcher.emit(eventName, pe); // only with multi-pointer if (this._pointers.length > 1) { if (eventName === 'up') { @@ -358,13 +466,13 @@ export class Pointers extends Class { // get the index for this pointer ID if multi-pointer is asked for var index = this._pointers.length > 1 ? this._getPointerIndex(e.pointerId) : 0; if (index === -1) { return; } - var x: number = e.pageX - Util.getPosition(this._engine.canvas).x; - var y: number = e.pageY - Util.getPosition(this._engine.canvas).y; - var transformedPoint = this._engine.screenToWorldCoordinates(new Vector(x, y)); - var pe = new PointerEvent(transformedPoint.x, transformedPoint.y, - e.pageX, e.pageY, x, y, index, this._stringToPointerType(e.pointerType), e.button, e); + + var pointer = this.at(index); + var coordinates = GlobalCoordinates.fromPagePosition(e.pageX, e.pageY, this._engine); + var pe = new PointerEvent(coordinates, pointer, index, this._stringToPointerType(e.pointerType), e.button, e); + eventArr.push(pe); - this.at(index).eventDispatcher.emit(eventName, pe); + pointer.eventDispatcher.emit(eventName, pe); // only with multi-pointer if (this._pointers.length > 1) { @@ -385,7 +493,7 @@ export class Pointers extends Class { return (e: MouseWheelEvent) => { // Should we prevent page scroll because of this event if (this._engine.pageScrollPreventionMode === ScrollPreventionMode.All || - (this._engine.pageScrollPreventionMode === ScrollPreventionMode.Canvas && e.target === this._engine.canvas)) { + (this._engine.pageScrollPreventionMode === ScrollPreventionMode.Canvas && e.target === this._engine.canvas)) { e.preventDefault(); } @@ -398,13 +506,13 @@ export class Pointers extends Class { // e.detail is only used in opera var deltaX = e.deltaX || - (e.wheelDeltaX * ScrollWheelNormalizationFactor) || - 0; + (e.wheelDeltaX * ScrollWheelNormalizationFactor) || + 0; var deltaY = e.deltaY || - (e.wheelDeltaY * ScrollWheelNormalizationFactor) || - (e.wheelDelta * ScrollWheelNormalizationFactor) || - e.detail || - 0; + (e.wheelDeltaY * ScrollWheelNormalizationFactor) || + (e.wheelDelta * ScrollWheelNormalizationFactor) || + e.detail || + 0; var deltaZ = e.deltaZ || 0; var deltaMode = WheelDeltaMode.Pixel; @@ -461,31 +569,76 @@ export class Pointers extends Class { */ export class Pointer extends Class { - constructor() { - super(); + private _isDown: boolean = false; + private _wasDown: boolean = false; - this.on('move', this._onPointerMove); + /** + * Whether the Pointer is currently dragging. + */ + public get isDragging(): boolean { + return this._isDown; + } + + /** + * Whether the Pointer just started dragging. + */ + public get isDragStart(): boolean { + return !this._wasDown && this._isDown; + } + + + /** + * Whether the Pointer just ended dragging. + */ + public get isDragEnd(): boolean { + return this._wasDown && !this._isDown; } /** * The last position on the document this pointer was at. Can be `null` if pointer was never active. */ - lastPagePos: Vector = null; + public lastPagePos: Vector = null; /** * The last position on the screen this pointer was at. Can be `null` if pointer was never active. */ - lastScreenPos: Vector = null; + public lastScreenPos: Vector = null; /** * The last position in the game world coordinates this pointer was at. Can be `null` if pointer was never active. */ - lastWorldPos: Vector = null; + public lastWorldPos: Vector = null; - private _onPointerMove(ev: PointerEvent) { - this.lastWorldPos = new Vector(ev.x, ev.y); + public actorsUnderPointer: { [ActorId: number]: Actor; } = {}; + + constructor() { + super(); + + this.on('move', this._onPointerMove); + this.on('down', this._onPointerDown); + this.on('up', this._onPointerUp); + } + + public update(): void { + if (this._wasDown && !this._isDown) { + this._wasDown = false; + } else if (!this._wasDown && this._isDown) { + this._wasDown = true; + } + } + + private _onPointerMove(ev: PointerEvent): void { this.lastPagePos = new Vector(ev.pageX, ev.pageY); this.lastScreenPos = new Vector(ev.screenX, ev.screenY); + this.lastWorldPos = new Vector(ev.x, ev.y); + } + + private _onPointerDown(): void { + this._isDown = true; + } + + private _onPointerUp(): void { + this._isDown = false; } } diff --git a/src/engine/Traits/CapturePointer.ts b/src/engine/Traits/CapturePointer.ts index d0ee8285b..738656e49 100644 --- a/src/engine/Traits/CapturePointer.ts +++ b/src/engine/Traits/CapturePointer.ts @@ -3,12 +3,17 @@ import { Actor } from '../Actor'; import { Engine } from '../Engine'; export interface ICapturePointerConfig { - + /** * Capture PointerMove events (may be expensive!) */ captureMoveEvents: boolean; + /** + * Capture PointerDrag events (may be expensive!) + */ + captureDragEvents: boolean; + } /** @@ -19,7 +24,7 @@ export class CapturePointer implements IActorTrait { public update(actor: Actor, engine: Engine) { if (!actor.enableCapturePointer) { return; } if (actor.isKilled()) { return; } - - engine.input.pointers.propogate(actor); + + engine.input.pointers.propogate(actor); } } \ No newline at end of file diff --git a/src/spec/ActorSpec.ts b/src/spec/ActorSpec.ts index ccd244b91..a264c855c 100644 --- a/src/spec/ActorSpec.ts +++ b/src/spec/ActorSpec.ts @@ -2,9 +2,9 @@ /// describe('A game actor', () => { - + var actor: ex.Actor; - + var engine: ex.Engine; var scene: ex.Scene; var mock = new Mocks.Mocker(); @@ -20,7 +20,7 @@ describe('A game actor', () => { spyOn(scene, 'debugDraw').and.callThrough(); spyOn(actor, 'draw'); - spyOn(actor, 'debugDraw'); + spyOn(actor, 'debugDraw'); ex.Physics.useBoxPhysics(); ex.Physics.acc.setTo(0, 0); @@ -91,7 +91,7 @@ describe('A game actor', () => { expect(actor.pos.x).toBe(20); expect(actor.pos.y).toBe(20); }); - + it('actors should generate pair hashes in the correct order', () => { var actor = new ex.Actor(); actor.id = 20; @@ -234,7 +234,7 @@ describe('A game actor', () => { actor.setHeight(100); actor.scale.setTo(2, 2); actor.anchor = new ex.Vector(0.5, 0.5); - + expect(actor.getLeft()).toBe(-100); expect(actor.getRight()).toBe(100); expect(actor.getTop()).toBe(-100); @@ -251,7 +251,7 @@ describe('A game actor', () => { var child = new ex.Actor(0, 0, 50, 50); actor.add(child); - + expect(child.getLeft()).toBe(-50); expect(child.getRight()).toBe(50); expect(child.getTop()).toBe(-50); @@ -259,7 +259,7 @@ describe('A game actor', () => { }); it('should have the correct bounds when scaled and rotated', () => { - + var actor = new ex.Actor(50, 50, 10, 10); // actor is now 20 high actor.scale.setTo(1, 2); @@ -276,7 +276,7 @@ describe('A game actor', () => { }); it('should have the correct relative bounds when scaled and rotated', () => { - + var actor = new ex.Actor(50, 50, 10, 10); // actor is now 20 high actor.scale.setTo(1, 2); @@ -291,7 +291,7 @@ describe('A game actor', () => { expect(bounds.top).toBeCloseTo(-5, .001); expect(bounds.bottom).toBeCloseTo(5, .001); }); - + it('has a left, right, top, and bottom when the anchor is (0, 0)', () => { actor.pos.x = 100; actor.pos.y = 100; @@ -320,7 +320,7 @@ describe('A game actor', () => { expect(actor.contains(21, 20)).toBe(false); expect(actor.contains(20, 21)).toBe(false); - + expect(actor.contains(0, -1)).toBe(false); expect(actor.contains(-1, 0)).toBe(false); }); @@ -366,11 +366,11 @@ describe('A game actor', () => { var actorCalled = 'false'; var otherCalled = 'false'; - actor.on('precollision', function() { + actor.on('precollision', function () { actorCalled = 'actor'; }); - other.on('precollision', function() { + other.on('precollision', function () { otherCalled = 'other'; }); @@ -381,7 +381,7 @@ describe('A game actor', () => { expect(actorCalled).toBe('actor'); expect(otherCalled).toBe('other'); - }); + }); it('is rotated along with its parent', () => { var rotation = ex.Util.toRadians(90); @@ -482,7 +482,7 @@ describe('A game actor', () => { expect(grandchild.getWorldPos().x).toBeCloseTo(10, 0.001); expect(grandchild.getWorldPos().y).toBeCloseTo(50, 0.001); - }); + }); it('can find its global coordinates if it has a parent', () => { expect(actor.pos.x).toBe(0); @@ -555,125 +555,125 @@ describe('A game actor', () => { expect(actor.capturePointer.captureMoveEvents).toBeTruthy(); expect(actor.enableCapturePointer).toBeTruthy(); }); - + it('changes opacity on color', () => { actor.color = ex.Color.Black.clone(); expect(actor.color.a).toBe(1); expect(actor.color.r).toBe(0); expect(actor.color.g).toBe(0); expect(actor.color.b).toBe(0); - + expect(actor.opacity).toBe(1.0); actor.opacity = .5; - + actor.update(engine, 100); expect(actor.color.a).toBe(.5); expect(actor.color.r).toBe(0); expect(actor.color.g).toBe(0); expect(actor.color.b).toBe(0); }); - + it('can detect containment off of child actors', () => { var parent = new ex.Actor(600, 100, 100, 100); var child = new ex.Actor(0, 0, 100, 100); var child2 = new ex.Actor(-600, -100, 100, 100); - + parent.add(child); child.add(child2); - + // check reality expect(parent.contains(550, 50)).toBeTruthy(); expect(parent.contains(650, 150)).toBeTruthy(); // in world coordinates this should be false expect(child.contains(50, 50)).toBeFalsy(); - + // in world coordinates this should be true expect(child.contains(550, 50)).toBeTruthy(); expect(child.contains(650, 150)).toBeTruthy(); - + // second order child shifted to the origin expect(child2.contains(-50, -50)).toBeTruthy(); - expect(child2.contains(50, 50)).toBeTruthy(); + expect(child2.contains(50, 50)).toBeTruthy(); }); - + it('can recursively check containment', () => { - var parent = new ex.Actor(0, 0, 100, 100); + var parent = new ex.Actor(0, 0, 100, 100); var child = new ex.Actor(100, 100, 100, 100); var child2 = new ex.Actor(100, 100, 100, 100); parent.add(child); - + expect(parent.contains(150, 150)).toBeFalsy(); expect(child.contains(150, 150)).toBeTruthy(); expect(parent.contains(150, 150, true)).toBeTruthy(); expect(parent.contains(200, 200, true)).toBeFalsy(); - + child.add(child2); - expect(parent.contains(250, 250, true)).toBeTruthy(); + expect(parent.contains(250, 250, true)).toBeTruthy(); }); it('with an active collision type can be placed on a fixed type', () => { ex.Physics.useBoxPhysics(); - var scene = new ex.Scene(engine); - + var scene = new ex.Scene(engine); + var active = new ex.Actor(0, -50, 100, 100); active.collisionType = ex.CollisionType.Active; active.vel.y = 10; active.acc.y = 1000; - + var fixed = new ex.Actor(-100, 50, 1000, 100); fixed.collisionType = ex.CollisionType.Fixed; - + scene.add(active); scene.add(fixed); - + expect(active.pos.x).toBe(0); expect(active.pos.y).toBe(-50); expect(fixed.pos.x).toBe(-100); expect(fixed.pos.y).toBe(50); - + // update many times for safety for (var i = 0; i < 40; i++) { scene.update(engine, 100); } - + expect(active.pos.x).toBeCloseTo(0, .0001); expect(active.pos.y).toBeCloseTo(-50, .0001); - + expect(fixed.pos.x).toBe(-100); expect(fixed.pos.y).toBe(50); }); - it('with an active collision type can jump on a fixed type', () => { + it('with an active collision type can jump on a fixed type', () => { var scene = new ex.Scene(engine); var active = new ex.Actor(0, -50, 100, 100); active.collisionType = ex.CollisionType.Active; active.vel.y = -100; ex.Physics.acc.setTo(0, 0); - + var fixed = new ex.Actor(-100, 50, 1000, 100); fixed.collisionType = ex.CollisionType.Fixed; - + scene.add(active); scene.add(fixed); - + expect(active.pos.x).toBe(0); expect(active.pos.y).toBe(-50); expect(fixed.pos.x).toBe(-100); expect(fixed.pos.y).toBe(50); - + var iterations = 1; // update many times for safety for (var i = 0; i < iterations; i++) { scene.update(engine, 1000); } - + expect(ex.Physics.acc.y).toBe(0); expect(active.pos.x).toBeCloseTo(0, .0001); expect(active.pos.y).toBeCloseTo(-100 * iterations + (-50) /* original y is -50 */, .0001); - + expect(fixed.pos.x).toBe(-100); expect(fixed.pos.y).toBe(50); }); @@ -683,40 +683,40 @@ describe('A game actor', () => { var childActor = new ex.Actor(); scene.add(parentActor); parentActor.add(childActor); - + spyOn(childActor, 'update'); - + scene.update(engine, 100); - + expect(childActor.update).toHaveBeenCalled(); }); - + it('draws visible child actors', () => { var parentActor = new ex.Actor(); var childActor = new ex.Actor(); scene.add(parentActor); parentActor.add(childActor); - + spyOn(childActor, 'draw'); - + childActor.visible = true; scene.draw(engine.ctx, 100); expect(childActor.draw).toHaveBeenCalled(); }); - + it('does not draw invisible child actors', () => { var parentActor = new ex.Actor(); var childActor = new ex.Actor(); scene.add(parentActor); parentActor.add(childActor); - + spyOn(childActor, 'draw'); - + childActor.visible = false; scene.draw(engine.ctx, 100); expect(childActor.draw).not.toHaveBeenCalled(); }); - + it('fires a killed event when killed', () => { var actor = new ex.Actor(); scene.add(actor); @@ -725,10 +725,10 @@ describe('A game actor', () => { killed = true; }); actor.kill(); - + expect(killed).toBe(true); }); - + it('is no longer killed when re-added to the game', () => { var actor = new ex.Actor(); scene.add(actor); @@ -754,11 +754,11 @@ describe('A game actor', () => { var preupdateFired = false; actor.on('preupdate', () => { preupdateFired = true; }); - actor.on('postupdate', () => { + actor.on('postupdate', () => { expect(preupdateFired).toBe(true); done(); }); - + scene.add(actor); scene.update(engine, 100); }); @@ -797,43 +797,434 @@ describe('A game actor', () => { var predrawedFired = false; actor.on('predraw', () => { predrawedFired = true; }); - actor.on('postdraw', () => { + actor.on('postdraw', () => { expect(predrawedFired).toBe(true); done(); }); - + scene.add(actor); scene.update(engine, 100); scene.draw(engine.ctx, 100); }); - it('opt into pointer capture when pointerup', () => { + it('should opt into pointer capture when pointerdown', () => { var actor = new ex.Actor(0, 0, 100, 100); - expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer caputer enabled'); - actor.on('pointerup', () => { - // doesn't matter; - }); - expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer caputer enabled after pointerup'); + + expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer capture enabled'); + expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not start with capturing move events'); + expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not start with capturing drag events'); + + actor.on('pointerdown', () => { /* doesn't matter; */ }); + + expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer capture enabled after pointerdown'); + expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not capture move events after pointerdown'); + expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not capture drag events after pointerdown'); }); - it('opt into pointer capture when pointerdown', () => { + it('should opt into pointer capture when pointerup', () => { var actor = new ex.Actor(0, 0, 100, 100); - expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer caputer enabled'); - actor.on('pointerdown', () => { - // doesn't matter; - }); - expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer caputer enabled after pointerdown'); + + expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer capture enabled'); + expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not start with capturing move events'); + expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not start with capturing drag events'); + + actor.on('pointerup', () => { /* doesn't matter; */ }); + + expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer capture enabled after pointerup'); + expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not capture move events after pointerdown'); + expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not capture drag events after pointerdown'); }); - it('opt into pointer capture when pointermove', () => { + it('should opt into pointer capture when pointermove', () => { var actor = new ex.Actor(0, 0, 100, 100); - expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer caputer enabled'); - actor.on('pointermove', () => { - // doesn't matter; - }); - expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer caputer enabled after pointermove'); + + expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer capture enabled'); + expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not start with capturing move events'); + expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not start with capturing drag events'); + + actor.on('pointermove', () => { /* doesn't matter; */ }); + + expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer capture enabled after pointermove'); expect(actor.capturePointer.captureMoveEvents).toBe(true, 'Actor should capture move events after pointermove'); + expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not capture drag events after pointermove'); + }); + + it('should opt into pointer capture when pointerenter', () => { + var actor = new ex.Actor(0, 0, 100, 100); + + expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer capture enabled'); + expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not start with capturing move events'); + expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not start with capturing drag events'); + + actor.on('pointerenter', () => { /* doesn't matter; */ }); + + expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer capture enabled after pointerenter'); + expect(actor.capturePointer.captureMoveEvents).toBe(true, 'Actor should capture move events after pointerenter'); + expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not capture drag events after pointerenter'); + }); + + it('should opt into pointer capture when pointerleave', () => { + var actor = new ex.Actor(0, 0, 100, 100); + + expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer capture enabled'); + expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not start with capturing move events'); + expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not start with capturing drag events'); + + actor.on('pointerleave', () => { /* doesn't matter; */ }); + + expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer capture enabled after pointerleave'); + expect(actor.capturePointer.captureMoveEvents).toBe(true, 'Actor should capture move events after pointerleave'); + expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not capture drag events after pointerleave'); + }); + it('should opt into pointer capture when pointerdragstart', () => { + var actor = new ex.Actor(0, 0, 100, 100); + + expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer capture enabled'); + expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not start with capturing move events'); + expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not start with capturing drag events'); + + actor.on('pointerdragstart', () => { /* doesn't matter; */ }); + + expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer capture enabled after pointerdragstart'); + expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should capture move events after pointerdragstart'); + expect(actor.capturePointer.captureDragEvents).toBe(true, 'Actors should capture drag events after pointerdragstart'); + }); + + it('opt into pointer capture when pointerdragend', () => { + var actor = new ex.Actor(0, 0, 100, 100); + + expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer capture enabled'); + expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not start with capturing move events'); + expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not start with capturing drag events'); + + actor.on('pointerdragend', () => { /* doesn't matter; */ }); + + expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer capture enabled after pointerdragend'); + expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should capture move events after pointerdragend'); + expect(actor.capturePointer.captureDragEvents).toBe(true, 'Actors should capture drag events after pointerdragend'); + }); + + it('should opt into pointer capture when pointerdragmove', () => { + var actor = new ex.Actor(0, 0, 100, 100); + + expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer capture enabled'); + expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not start with capturing move events'); + expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not start with capturing drag events'); + + actor.on('pointerdragmove', () => { /* doesn't matter; */ }); + + expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer capture enabled after pointerdragmove'); + expect(actor.capturePointer.captureMoveEvents).toBe(true, 'Actors should capture move events after pointerdragmove'); + expect(actor.capturePointer.captureDragEvents).toBe(true, 'Actors should capture drag events after pointerdragmove'); + }); + + it('should opt into pointer capture when pointerdragenter', () => { + var actor = new ex.Actor(0, 0, 100, 100); + + expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer capture enabled'); + expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not start with capturing move events'); + expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not start with capturing drag events'); + + actor.on('pointerdragenter', () => { /* doesn't matter; */ }); + + expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer capture enabled after pointerdragenter'); + expect(actor.capturePointer.captureMoveEvents).toBe(true, 'Actors should capture move events after pointerdragenter'); + expect(actor.capturePointer.captureDragEvents).toBe(true, 'Actors should capture drag events after pointerdragenter'); + }); + + it('should opt into pointer capture when pointerdragleave', () => { + var actor = new ex.Actor(0, 0, 100, 100); + + expect(actor.enableCapturePointer).toBe(false, 'Actors start without pointer capture enabled'); + expect(actor.capturePointer.captureMoveEvents).toBe(false, 'Actors should not start with capturing move events'); + expect(actor.capturePointer.captureDragEvents).toBe(false, 'Actors should not start with capturing drag events'); + + actor.on('pointerdragleave', () => { /* doesn't matter; */ }); + + expect(actor.enableCapturePointer).toBe(true, 'Actors should have pointer capture enabled after pointerdragleave'); + expect(actor.capturePointer.captureMoveEvents).toBe(true, 'Actors should capture move events after pointerdragleave'); + expect(actor.capturePointer.captureDragEvents).toBe(true, 'Actors should capture drag events after pointerdragleave'); + }); + + it('should capture pointer move event', () => { + var actor = new ex.Actor(0, 0, 20, 20); + var pointerEvent: any; + var callables = { move: (pe: any) => pointerEvent = pe }; + var moveSpy = spyOn(callables, 'move').and.callThrough(); + + actor.on('pointermove', callables.move); + + scene.add(actor); + + (engine.input.pointers)._pointerMove.push({ + pointer: { + lastWorldPos: new ex.Vector(5, -5), + actorsUnderPointer: {} + } + }); + + scene.update(engine, 100); + + expect(moveSpy).toHaveBeenCalledTimes(1); + expect(pointerEvent.pointer.lastWorldPos.x).toEqual(5, 'pointer event should contain correct world position x'); + expect(pointerEvent.pointer.lastWorldPos.y).toEqual(-5, 'pointer event should contain correct world position y'); + }); + + it('should capture pointer enter event', () => { + var actor = new ex.Actor(0, 0, 20, 20); + var pointerEvent: any; + var callables = { enter: (pe: any) => pointerEvent = pe }; + var enterSpy = spyOn(callables, 'enter').and.callThrough(); + + actor.on('pointerenter', callables.enter); + + scene.add(actor); + + (engine.input.pointers)._pointerMove.push({ + pointer: { + lastWorldPos: new ex.Vector(5, -5), + actorsUnderPointer: {} + } + }); + + scene.update(engine, 100); + + (engine.input.pointers)._pointerMove.push({ + pointer: { + lastWorldPos: new ex.Vector(5, -5), + actorsUnderPointer: (() => { + let obj = {}; + + obj[actor.id] = actor; + + return obj; + })() + } + }); + + scene.update(engine, 100); + + expect(enterSpy).toHaveBeenCalledTimes(1); + expect(pointerEvent.pointer.lastWorldPos.x).toEqual(5, 'pointer event should contain correct world position x'); + expect(pointerEvent.pointer.lastWorldPos.y).toEqual(-5, 'pointer event should contain correct world position y'); + }); + + it('should capture pointer leave event', () => { + var actor = new ex.Actor(0, 0, 20, 20); + var pointerEvent: any; + var callables = { leave: (pe: any) => pointerEvent = pe }; + var leaveSpy = spyOn(callables, 'leave').and.callThrough(); + + actor.on('pointerleave', callables.leave); + + scene.add(actor); + + (engine.input.pointers)._pointerMove.push({ + pointer: { + lastWorldPos: new ex.Vector(5, -5), + actorsUnderPointer: {} + } + }); + + scene.update(engine, 100); + + (engine.input.pointers)._pointerMove.push({ + pointer: { + lastWorldPos: new ex.Vector(15, -15), + actorsUnderPointer: (() => { + let obj = {}; + + obj[actor.id] = actor; + + return obj; + })() + } + }); + + scene.update(engine, 100); + scene.update(engine, 100); + + expect(leaveSpy).toHaveBeenCalledTimes(1); + expect(pointerEvent.pointer.lastWorldPos.x).toEqual(15, 'pointer event should contain correct world position x'); + expect(pointerEvent.pointer.lastWorldPos.y).toEqual(-15, 'pointer event should contain correct world position y'); + }); + + it('should capture pointer drag start event', () => { + var actor = new ex.Actor(0, 0, 20, 20); + var pointerEvent: any; + var callables = { dragStart: (pe: any) => pointerEvent = pe }; + var dragStartSpy = spyOn(callables, 'dragStart').and.callThrough(); + + actor.on('pointerdragstart', callables.dragStart); + + scene.add(actor); + + (engine.input.pointers)._pointerDown.push({ + pointer: { + isDragStart: true, + lastWorldPos: new ex.Vector(5, -5), + actorsUnderPointer: {} + } + }); + + scene.update(engine, 100); + + expect(dragStartSpy).toHaveBeenCalledTimes(1); + expect(pointerEvent.pointer.lastWorldPos.x).toEqual(5, 'pointer event should contain correct world position x'); + expect(pointerEvent.pointer.lastWorldPos.y).toEqual(-5, 'pointer event should contain correct world position y'); + }); + + it('should capture pointer drag end event', () => { + var actor = new ex.Actor(0, 0, 20, 20); + var pointerEvent: any; + var callables = { dragEnd: (pe: any) => pointerEvent = pe }; + var dragEndSpy = spyOn(callables, 'dragEnd').and.callThrough(); + + actor.on('pointerdragend', callables.dragEnd); + + scene.add(actor); + + (engine.input.pointers)._pointerDown.push({ + pointer: { + isDragStart: true, + lastWorldPos: new ex.Vector(5, -5), + actorsUnderPointer: {} + } + }); + + scene.update(engine, 100); + + (engine.input.pointers)._pointerUp.push({ + pointer: { + isDragEnd: true, + lastWorldPos: new ex.Vector(5, -5), + actorsUnderPointer: {} + } + }); + + scene.update(engine, 100); + + expect(dragEndSpy).toHaveBeenCalledTimes(1); + expect(pointerEvent.pointer.lastWorldPos.x).toEqual(5, 'pointer event should contain correct world position x'); + expect(pointerEvent.pointer.lastWorldPos.y).toEqual(-5, 'pointer event should contain correct world position y'); + }); + + it('should capture pointer drag move event', () => { + var actor = new ex.Actor(0, 0, 20, 20); + var pointerEvent: any; + var callables = { dragMove: (pe: any) => pointerEvent = pe }; + var dragMoveSpy = spyOn(callables, 'dragMove').and.callThrough(); + + actor.on('pointerdragmove', callables.dragMove); + + scene.add(actor); + + (engine.input.pointers)._pointerMove.push({ + pointer: { + isDragging: true, + lastWorldPos: new ex.Vector(5, -5), + actorsUnderPointer: {} + } + }); + + scene.update(engine, 100); + + expect(dragMoveSpy).toHaveBeenCalledTimes(1); + expect(pointerEvent.pointer.lastWorldPos.x).toEqual(5, 'pointer event should contain correct world position x'); + expect(pointerEvent.pointer.lastWorldPos.y).toEqual(-5, 'pointer event should contain correct world position y'); + }); + + it('should capture pointer drag enter event', () => { + var actor = new ex.Actor(0, 0, 20, 20); + var pointerEvent: any; + var callables = { dragEnter: (pe: any) => pointerEvent = pe }; + var dragEnterSpy = spyOn(callables, 'dragEnter').and.callThrough(); + + actor.on('pointerdragenter', callables.dragEnter); + + scene.add(actor); + + (engine.input.pointers)._pointerMove.push({ + pointer: { + isDragging: true, + lastWorldPos: new ex.Vector(5, -5), + actorsUnderPointer: {} + } + }); + + scene.update(engine, 100); + + (engine.input.pointers)._pointerMove.push({ + pointer: { + isDragging: true, + lastWorldPos: new ex.Vector(5, -5), + actorsUnderPointer: (() => { + let obj = {}; + + obj[actor.id] = actor; + + return obj; + })() + } + }); + + scene.update(engine, 100); + + expect(dragEnterSpy).toHaveBeenCalledTimes(1); + expect(pointerEvent.pointer.lastWorldPos.x).toEqual(5, 'pointer event should contain correct world position x'); + expect(pointerEvent.pointer.lastWorldPos.y).toEqual(-5, 'pointer event should contain correct world position y'); + }); + + it('should capture pointer drag leave event', () => { + var actor = new ex.Actor(0, 0, 20, 20); + var pointerEvent: any; + var callables = { dragLeave: (pe: any) => pointerEvent = pe }; + var dragLeaveSpy = spyOn(callables, 'dragLeave').and.callThrough(); + + actor.on('pointerdragleave', callables.dragLeave); + + scene.add(actor); + + (engine.input.pointers)._pointerMove.push({ + pointer: { + isDragging: true, + lastWorldPos: new ex.Vector(5, -5), + actorsUnderPointer: {} + } + }); + + scene.update(engine, 100); + + (engine.input.pointers)._pointerMove.push({ + pointer: { + isDragging: true, + lastWorldPos: new ex.Vector(15, -15), + actorsUnderPointer: (() => { + let obj = {}; + + obj[actor.id] = actor; + + return obj; + })() + } + }); + + scene.update(engine, 100); + + (engine.input.pointers)._pointerMove.push({ + pointer: { + isDragging: true, + lastWorldPos: new ex.Vector(15, -15), + actorsUnderPointer: {} + } + }); + + scene.update(engine, 100); + + expect(dragLeaveSpy).toHaveBeenCalledTimes(1); + expect(pointerEvent.pointer.lastWorldPos.x).toEqual(15, 'pointer event should contain correct world position x'); + expect(pointerEvent.pointer.lastWorldPos.y).toEqual(-15, 'pointer event should contain correct world position y'); }); it('only has pointer events happen once per frame', () => { @@ -842,17 +1233,21 @@ describe('A game actor', () => { var numPointerUps = 0; var propSpy = spyOn(engine.input.pointers, 'propogate').and.callThrough(); - (engine.input.pointers)._pointerUp.push({x: 0, y: 0}); + (engine.input.pointers)._pointerUp.push({ + pointer: { + lastWorldPos: { x: 0, y: 0 } + } + }); actor.on('pointerup', () => { numPointerUps++; }); scene.add(actor); - + scene.update(engine, 100); expect(numPointerUps).toBe(1, 'Pointer up should be triggered once'); - expect(propSpy.calls.count()).toEqual(1, 'Propogate should be called once'); + expect(propSpy).toHaveBeenCalledTimes(1); }); });