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);
}
}