diff --git a/CHANGELOG.md b/CHANGELOG.md index c633683b1..ffea94642 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,14 +8,14 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Breaking Changes -- Renamed Utils.removeItemFromArray() to Utils.removeItemFromArray() ([#798](https://github.com/excaliburjs/Excalibur/issues/798/)] +- Renamed Utils.removeItemFromArray() to Utils.removeItemFromArray() ([#798](https://github.com/excaliburjs/Excalibur/issues/798/)) ### Added -- Added optional volume argument to `Sound.play(volume?: number)`, which will play the Audio file -at anywhere from mute (`volume` is 0.0) to full volume (`volume` is 1.0). ([#801](https://github.com/excaliburjs/Excalibur/issues/801)) +- Added optional volume argument to `Sound.play(volume?: number)`, which will play the Audio file at anywhere from mute (`volume` is 0.0) to full volume (`volume` is 1.0). ([#801](https://github.com/excaliburjs/Excalibur/issues/801)) - Added another DisplayMode option: `DisplayMode.Position`. When this is selected as the displayMode type, the user must specify a new `position` option - Added a new Interface `AbsolutePosition` which can described the `position` option. A `string` can also describe `position` -- Added a static method distanceBetweenVectors to the Vector class (#517)[https://github.com/excaliburjs/Excalibur/issues/517] +- Added a static method distanceBetweenVectors to the Vector class ([#517](https://github.com/excaliburjs/Excalibur/issues/517)) +- Added `PointerWheel` event type for the `wheel` browser event, Excalibur now supports scroll wheel ([#808](https://github.com/excaliburjs/Excalibur/issues/808/)) ### Changed - Edge builds have more descriptive versions now containing build number and Git commit hash (e.g. `0.10.0-alpha.105#commit`) ([#777](https://github.com/excaliburjs/Excalibur/issues/777)) diff --git a/sandbox/tests/input/pointer.html b/sandbox/tests/input/pointer.html index d4cedfec2..81ab98002 100644 --- a/sandbox/tests/input/pointer.html +++ b/sandbox/tests/input/pointer.html @@ -27,6 +27,8 @@
Last Pointer World Pos (x,y)
+
Last Wheel Deltas (x,y,z,mode)
+
diff --git a/sandbox/tests/input/pointer.ts b/sandbox/tests/input/pointer.ts index 9d0fb326f..a2b9c69d4 100644 --- a/sandbox/tests/input/pointer.ts +++ b/sandbox/tests/input/pointer.ts @@ -47,6 +47,10 @@ box.on("pointerdown", (pe: ex.Input.PointerEvent) => { boxPointerDown = true; }); +box.on("pointerwheel", (pe: ex.Input.WheelEvent) => { + box.rotation = box.rotation + (pe.deltaY > 0 ? 0.1 : -0.1); +}); + // Follow cursor game.input.pointers.primary.on("move", (pe: ex.Input.PointerEvent) => { cursor.pos.x = pe.x; @@ -61,6 +65,17 @@ game.input.pointers.primary.on("up", (pe: ex.Input.PointerEvent) => { document.getElementById("pointer-btn").innerHTML = ""; }); +// Wheel +game.input.pointers.primary.on("wheel", (pe: ex.Input.WheelEvent) => { + var type: string; + switch (pe.deltaMode) { + case ex.Input.WheelDeltaMode.Pixel: type = "pixel"; break; + case ex.Input.WheelDeltaMode.Line: type = "line"; break; + case ex.Input.WheelDeltaMode.Page: type = "page"; break; + } + document.getElementById("pointer-wheel-deltas").innerHTML = pe.deltaX + ", " + pe.deltaY + ", " + pe.deltaZ + ", " + type; +}); + var paintBrush = { paint: (x: number, y: number, color: ex.Color) => { var brush = new ex.Actor(x, y, 5, 5, color); @@ -104,4 +119,4 @@ game.currentScene.camera.y = 0; game.add(box); game.add(cursor); game.add(uiElement); -game.start(); \ No newline at end of file +game.start(); diff --git a/src/engine/Actor.ts b/src/engine/Actor.ts index ff7c1d3ee..086901327 100644 --- a/src/engine/Actor.ts +++ b/src/engine/Actor.ts @@ -477,6 +477,7 @@ export class Actor extends Class implements IActionable, IEvented { public on(eventName: Events.pointerdown, 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: string, handler: (event?: GameEvent) => void): void; public on(eventName: string, handler: (event?: GameEvent) => void): void { this._checkForPointerOptIn(eventName); @@ -1123,4 +1124,4 @@ export enum CollisionType { * collision events. */ Fixed -} \ No newline at end of file +} diff --git a/src/engine/Docs/Pointers.md b/src/engine/Docs/Pointers.md index 63f31fe5a..37d020702 100644 --- a/src/engine/Docs/Pointers.md +++ b/src/engine/Docs/Pointers.md @@ -26,6 +26,17 @@ engine.input.pointers.primary.on("move", function (evt) { }); engine.input.pointers.primary.on("cancel", function (evt) { }); ``` +### Wheel Event + +You can also subscribe to the mouse wheel event through `engine.input.pointers.on`. A [[WheelEvent]] +object is passed to your handler which offers information about the wheel event being received. + +- `wheel` - When a mousewheel is activated (trackpad scroll or mouse wheel) + +```js +engine.input.pointers.on("wheel", function (evt) { }); +``` + ## Last position querying If you don't wish to subscribe to events, you can also access the [[Pointer.lastPagePos]], [[Pointer.lastScreenPos]] @@ -129,4 +140,4 @@ player.capturePointer.captureMoveEvents = true; player.on("pointerup", function (ev) { player.logger.info("Player selected!", ev); }); -``` \ No newline at end of file +``` diff --git a/src/engine/Engine.ts b/src/engine/Engine.ts index d770ada8b..83068ead1 100644 --- a/src/engine/Engine.ts +++ b/src/engine/Engine.ts @@ -47,6 +47,24 @@ export enum DisplayMode { Position } +/** + * Enum representing the different mousewheel event bubble prevention + */ +export enum ScrollPreventionMode { + /** + * Do not prevent any page scrolling + */ + None, + /** + * Prevent page scroll if mouse is over the game canvas + */ + Canvas, + /** + * Prevent all page scrolling via mouse wheel + */ + All +} + /* * Interface describing the absolute CSS position of the game window. For use when DisplayMode.Position * is specified and when the user wants to define exact pixel spacing of the window. @@ -112,6 +130,11 @@ export interface IEngineOptions { */ position?: string | IAbsolutePosition; + + /** + * Scroll prevention method. + */ + scrollPreventionMode?: ScrollPreventionMode; } /** @@ -234,6 +257,11 @@ export class Engine extends Class { */ public onFatalException = (e: any) => { Logger.getInstance().fatal(e); }; + /** + * The mouse wheel scroll prevention mode + */ + public pageScrollPreventionMode: ScrollPreventionMode; + private _logger: Logger; private _isSmoothingEnabled: boolean = true; @@ -268,12 +296,13 @@ export class Engine extends Class { * Default [[IEngineOptions]] */ private static _DefaultEngineOptions: IEngineOptions = { - width: 0, - height: 0, - canvasElementId: '', - pointerScope: Input.PointerScope.Document, - suppressConsoleBootMessage: null, - suppressMinimumBrowserFeatureDetection: null + width: 0, + height: 0, + canvasElementId: '', + pointerScope: Input.PointerScope.Document, + suppressConsoleBootMessage: null, + suppressMinimumBrowserFeatureDetection: null, + scrollPreventionMode: ScrollPreventionMode.Canvas }; /** @@ -875,6 +904,8 @@ O|===|* >________________>\n\ this.input.pointers.init(options && options.pointerScope === Input.PointerScope.Document ? document : this.canvas); this.input.gamepads.init(); + this.pageScrollPreventionMode = options.scrollPreventionMode; + // Issue #385 make use of the visibility api // https://developer.mozilla.org/en-US/docs/Web/Guide/User_experience/Using_the_Page_Visibility_API diff --git a/src/engine/Events.ts b/src/engine/Events.ts index d045190e8..147dfa904 100644 --- a/src/engine/Events.ts +++ b/src/engine/Events.ts @@ -49,11 +49,13 @@ export type pointerup = 'pointerup'; export type pointerdown = 'pointerdown'; export type pointermove = 'pointermove'; export type pointercancel = 'pointercancel'; +export type pointerwheel = 'pointerwheel'; export type up = 'up'; export type down = 'down'; export type move = 'move'; export type cancel = 'cancel'; +export type wheel = 'wheel'; export type press = 'press'; export type release = 'release'; @@ -336,4 +338,4 @@ export class EnterViewPortEvent extends GameEvent { constructor(public target: Actor) { super(); } -} \ No newline at end of file +} diff --git a/src/engine/Input/Pointer.ts b/src/engine/Input/Pointer.ts index 3c9cea975..b0482583f 100644 --- a/src/engine/Input/Pointer.ts +++ b/src/engine/Input/Pointer.ts @@ -1,4 +1,4 @@ -import { Engine } from './../Engine'; +import {Engine, ScrollPreventionMode} from './../Engine'; import { GameEvent } from '../Events'; import { UIActor } from '../UIActor'; import { Vector } from '../Algebra'; @@ -26,6 +26,12 @@ export enum PointerButton { Unknown } +export enum WheelDeltaMode { + Pixel, + Line, + Page +} + /** * Determines the scope of handling mouse/touch events. See [[Pointers]] for more information. */ @@ -43,6 +49,13 @@ export enum PointerScope { Document } +/** + * A constant used to normalize wheel events across different browsers + * + * This normalization factor is pulled from https://developer.mozilla.org/en-US/docs/Web/Events/wheel#Listening_to_this_event_across_browser + */ +const ScrollWheelNormalizationFactor = -1 / 40; + /** * Pointer events * @@ -65,22 +78,60 @@ export class PointerEvent extends GameEvent { * @param button The button pressed (if [[PointerType.Mouse]]) * @param ev The raw DOM event being handled */ - constructor(public x: number, - public y: number, + 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 index: number, + public pointerType: PointerType, + public button: PointerButton, public ev: any) { super(); } }; /** - * Handles pointer events (mouse, touch, stylus, etc.) and normalizes to + * Wheel Events + * + * Represents a mouse wheel event. See [[Pointers]] for more information on + * handling point input. + */ +export class WheelEvent extends GameEvent { + + /** + * @param x The `x` coordinate of the event (in world coordinates) + * @param y The `y` coordinate of the event (in world coordinates) + * @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 deltaX The type of pointer + * @param deltaY The type of pointer + * @param deltaZ The type of pointer + * @param deltaMode The type of movement [[WheelDeltaMode]] + * @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) { + super(); + } +}; + +/** + * Handles pointer events (mouse, touch, stylus, etc.) and normalizes to * [W3C Pointer Events](http://www.w3.org/TR/pointerevents/). * * [[include:Pointers.md]] @@ -92,6 +143,7 @@ export class Pointers extends Class { private _pointerUp: PointerEvent[] = []; private _pointerMove: PointerEvent[] = []; private _pointerCancel: PointerEvent[] = []; + private _wheel: WheelEvent[] = []; private _pointers: Pointer[] = []; private _activePointers: number[] = []; @@ -108,6 +160,7 @@ export class Pointers extends Class { public on(eventName: Events.down, handler: (event?: PointerEvent) => void): void; public on(eventName: Events.move, 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; public on(eventName: string, handler: (event?: GameEvent) => void): void { super.on(eventName, handler); @@ -117,7 +170,7 @@ export class Pointers extends Class { * Primary pointer (mouse, 1 finger, stylus, etc.) */ public primary: Pointer; - + /** * Initializes pointer event listeners */ @@ -155,6 +208,18 @@ export class Pointers extends Class { target.addEventListener('mouseup', this._handleMouseEvent('up', this._pointerUp)); target.addEventListener('mousemove', this._handleMouseEvent('move', this._pointerMove)); } + + // MDN MouseWheelEvent + if ('onwheel' in document.createElement('div')) { + // Modern Browsers + target.addEventListener('wheel', this._handleWheelEvent('wheel', this._wheel)); + } else if (document.onmousewheel !== undefined) { + // Webkit and IE + target.addEventListener('mousewheel', this._handleWheelEvent('wheel', this._wheel)); + } else { + // Remaining browser and older Firefox + target.addEventListener('MozMousePixelScroll', this._handleWheelEvent('wheel', this._wheel)); + } } public update(): void { @@ -162,8 +227,9 @@ export class Pointers extends Class { this._pointerDown.length = 0; this._pointerMove.length = 0; this._pointerCancel.length = 0; + this._wheel.length = 0; } - + /** * Safely gets a Pointer at a specific index and initializes one if it doesn't yet exist * @param index The pointer index to retrieve @@ -193,7 +259,7 @@ export class Pointers extends Class { */ public propogate(actor: any) { var isUIActor = actor instanceof UIActor; - var i: number = 0, + var i: number = 0, len: number = this._pointerUp.length; for (i; i < len; i++) { @@ -231,6 +297,14 @@ export class Pointers extends Class { actor.eventDispatcher.emit('pointercancel', this._pointerCancel[i]); } } + + i = 0; + len = this._wheel.length; + for (i; i < len; i++) { + if (actor.contains(this._wheel[i].x, this._wheel[i].y, !isUIActor)) { + actor.eventDispatcher.emit('pointerwheel', this._wheel[i]); + } + } } private _handleMouseEvent(eventName: string, eventArr: PointerEvent[]) { @@ -254,7 +328,7 @@ export class Pointers extends Class { 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, + var pe = new PointerEvent(transformedPoint.x, transformedPoint.y, e.changedTouches[i].pageX, e.changedTouches[i].pageY, x, y, index, PointerType.Touch, PointerButton.Unknown, e); eventArr.push(pe); this.at(index).eventDispatcher.emit(eventName, pe); @@ -277,14 +351,14 @@ export class Pointers extends Class { private _handlePointerEvent(eventName: string, eventArr: PointerEvent[]) { return (e: MSPointerEvent) => { e.preventDefault(); - + // 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, + var pe = new PointerEvent(transformedPoint.x, transformedPoint.y, e.pageX, e.pageY, x, y, index, this._stringToPointerType(e.pointerType), e.button, e); eventArr.push(pe); this.at(index).eventDispatcher.emit(eventName, pe); @@ -304,6 +378,49 @@ export class Pointers extends Class { }; } + private _handleWheelEvent(eventName: string, eventArr: WheelEvent[]) { + 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)) { + 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)); + + // deltaX, deltaY, and deltaZ are the standard modern properties + // wheelDeltaX, wheelDeltaY, are legacy properties in webkit browsers and older IE + // e.detail is only used in opera + + var deltaX = e.deltaX || + (e.wheelDeltaX * ScrollWheelNormalizationFactor) || + 0; + var deltaY = e.deltaY || + (e.wheelDeltaY * ScrollWheelNormalizationFactor) || + (e.wheelDelta * ScrollWheelNormalizationFactor) || + e.detail || + 0; + var deltaZ = e.deltaZ || 0; + var deltaMode = WheelDeltaMode.Pixel; + + if (e.deltaMode) { + if (e.deltaMode === 1) { + deltaMode = WheelDeltaMode.Line; + } else if (e.deltaMode === 2) { + deltaMode = WheelDeltaMode.Page; + } + } + + var we = new WheelEvent(transformedPoint.x, transformedPoint.y, + e.pageX, e.pageY, x, y, 0, deltaX, deltaY, deltaZ, deltaMode, e); + + eventArr.push(we); + this.at(0).eventDispatcher.emit(eventName, we); + }; + } + /** * Gets the index of the pointer specified for the given pointer ID or finds the next empty pointer slot available. * This is required because IE10/11 uses incrementing pointer IDs so we need to store a mapping of ID => idx @@ -340,10 +457,10 @@ export class Pointers extends Class { * Captures and dispatches PointerEvents */ export class Pointer extends Class { - + constructor() { super(); - + this.on('move', this._onPointerMove); } @@ -360,12 +477,12 @@ export class Pointer extends Class { /** * The last position in the game world coordinates this pointer was at. Can be `null` if pointer was never active. */ - lastWorldPos: Vector = null; + lastWorldPos: Vector = null; private _onPointerMove(ev: PointerEvent) { this.lastWorldPos = new Vector(ev.x, ev.y); this.lastPagePos = new Vector(ev.pageX, ev.pageY); - this.lastScreenPos = new Vector(ev.screenX, ev.screenY); + this.lastScreenPos = new Vector(ev.screenX, ev.screenY); } }