forked from sent/waves
1086 lines
37 KiB
TypeScript
1086 lines
37 KiB
TypeScript
/// <reference path="InputWritr-0.2.0.ts" />
|
|
|
|
declare module TouchPassr {
|
|
/**
|
|
* Schema for where a control should lay on the screen.
|
|
*/
|
|
export interface IPosition {
|
|
vertical: string;
|
|
horizontal: string;
|
|
offset?: IPositionOffset
|
|
}
|
|
|
|
/**
|
|
* Offset measurements for a schema's position.
|
|
*/
|
|
export interface IPositionOffset {
|
|
left?: number | string;
|
|
top?: number | string;
|
|
}
|
|
|
|
/**
|
|
* Global declaration of styles for all controls, typically passed from a
|
|
* TouchPassr to its generated controls.
|
|
*/
|
|
export interface IRootControlStyles {
|
|
global: IControlStyles;
|
|
Button: IButtonStyles;
|
|
Joystick: IJoystickStyles;
|
|
}
|
|
|
|
/**
|
|
* Container for controls, keyed by name.
|
|
*/
|
|
export interface IControlsContainer {
|
|
[i: string]: Control;
|
|
}
|
|
|
|
/**
|
|
* Container for control schemas, keyed by name.
|
|
*/
|
|
export interface IControlSchemasContainer {
|
|
[i: string]: IControlSchema;
|
|
}
|
|
|
|
/**
|
|
* General container for element styles of a control. It should be extended
|
|
* for more specific controls.
|
|
*/
|
|
export interface IControlStyles {
|
|
element?: CSSRuleList;
|
|
elementInner?: CSSRuleList;
|
|
}
|
|
|
|
/**
|
|
* Root schema to be followed for all controls. More specific schema versions
|
|
* will extend this.
|
|
*/
|
|
export interface IControlSchema {
|
|
name: string;
|
|
control: string;
|
|
position: IPosition;
|
|
label?: string;
|
|
styles?: IControlStyles;
|
|
}
|
|
|
|
/**
|
|
* Schema for how a control should interact with its InputWriter. Each member key
|
|
* is the control action, which is linked to any number of InputWriter events, each
|
|
* of which contains any number of key codes to send.
|
|
*/
|
|
export interface IPipes {
|
|
activated?: { [i: string]: (string | number)[] };
|
|
deactivated?: { [i: string]: (string | number)[] };
|
|
}
|
|
|
|
/**
|
|
* Control schema for a simple button. Pipes are activated on press and on release.
|
|
*/
|
|
export interface IButtonSchema extends IControlSchema {
|
|
pipes?: IPipes;
|
|
}
|
|
|
|
/**
|
|
* Styles schema for a button control, which doesn't change anything.
|
|
*/
|
|
export interface IButtonStyles extends IControlStyles { }
|
|
|
|
/**
|
|
* Control schema for a joystick. It may have any number of directions that it
|
|
* will snap to, each of which will have its own pipes.
|
|
*/
|
|
export interface IJoystickSchema extends IControlSchema {
|
|
directions: IJoystickDirection[];
|
|
}
|
|
|
|
/**
|
|
* Schema for a single direction for a joystick. It will be represented as a tick
|
|
* on the joystick that the control will snap its direction to.
|
|
*/
|
|
export interface IJoystickDirection {
|
|
name: string;
|
|
degrees: number;
|
|
neighbors?: string[];
|
|
pipes?: IPipes;
|
|
}
|
|
|
|
/**
|
|
* Styles schema for a joystick control, adding its ticks and indicator elements.
|
|
*/
|
|
export interface IJoystickStyles extends IControlStyles {
|
|
circle?: IControlStyles;
|
|
tick?: IControlStyles;
|
|
dragLine?: IControlStyles;
|
|
dragShadow?: IControlStyles;
|
|
}
|
|
|
|
export interface ITouchPassrSettings {
|
|
InputWriter: InputWritr.IInputWritr;
|
|
prefix?: string;
|
|
container?: HTMLElement;
|
|
styles?: any;
|
|
controls?: { [i: string]: IControlSchema };
|
|
enabled?: boolean;
|
|
}
|
|
|
|
export interface ITouchPassr {
|
|
getInputWriter(): InputWritr.IInputWritr;
|
|
getEnabled(): boolean;
|
|
getStyles(): IRootControlStyles;
|
|
getControls(): IControlsContainer;
|
|
getContainer(): HTMLElement;
|
|
getParentContainer(): HTMLElement;
|
|
enable(): void;
|
|
disable(): void;
|
|
setParentContainer(parentElement: HTMLElement): void;
|
|
addControls(schemas: IControlSchemasContainer): void;
|
|
addControl(schema: IControlSchema): void;
|
|
}
|
|
}
|
|
|
|
|
|
module TouchPassr {
|
|
"use strict";
|
|
|
|
/**
|
|
* Abstract class for on-screen controls. Element creation for .element
|
|
* and .elementInner within the constrained position is provided.
|
|
*/
|
|
export class Control {
|
|
/**
|
|
* The parent TouchPassr's InputWritr. Pipe events are sent through here.
|
|
*/
|
|
protected InputWriter: InputWritr.IInputWritr;
|
|
|
|
/**
|
|
* The governing schema for this control. It should be overriden as a more
|
|
* specific shema in child classes.
|
|
*/
|
|
protected schema: IControlSchema;
|
|
|
|
/**
|
|
* The outer container element. It should have width and height of 0, so
|
|
* it can be positioned using the schema's .position.
|
|
*/
|
|
protected element: HTMLElement;
|
|
|
|
/**
|
|
* The inner container element, directly inside the outer container. It
|
|
* should be positioned absolutely so its center is the outer container.
|
|
*/
|
|
protected elementInner: HTMLElement;
|
|
|
|
/**
|
|
* Resets the control by setting member variables and calling resetElement.
|
|
*
|
|
* @param {InputWritr} InputWriter
|
|
* @param {Object} schema
|
|
*/
|
|
constructor(InputWriter: InputWritr.IInputWritr, schema: IControlSchema, styles: IRootControlStyles) {
|
|
this.InputWriter = InputWriter;
|
|
this.schema = schema;
|
|
this.resetElement(styles);
|
|
}
|
|
|
|
/**
|
|
* @return {HTMLElement} The outer container element.
|
|
*/
|
|
public getElement(): HTMLElement {
|
|
return this.element;
|
|
}
|
|
|
|
/**
|
|
* @return {HTMLElement} The inner container element.
|
|
*/
|
|
public getElementInner(): HTMLElement {
|
|
return this.elementInner;
|
|
}
|
|
|
|
/**
|
|
* Creates and returns an HTMLElement of the specified type. Any additional
|
|
* settings Objects may be given to be proliferated onto the Element via
|
|
* proliferateElement.
|
|
*
|
|
* @param {String} type The tag of the Element to be created.
|
|
* @param {Object} [settings] Additional settings for the Element, such as
|
|
* className or style.
|
|
* @return {HTMLElement}
|
|
*/
|
|
public createElement(tag: string, ...args: any[]): HTMLElement {
|
|
var element: any = document.createElement(tag || "div"),
|
|
i: number;
|
|
|
|
// For each provided object, add those settings to the element
|
|
for (i = 1; i < arguments.length; i += 1) {
|
|
this.proliferateElement(element, arguments[i]);
|
|
}
|
|
|
|
return element;
|
|
}
|
|
|
|
/**
|
|
* Identical to proliferate, but tailored for HTML elements because many
|
|
* element attributes don't play nicely with JavaScript Array standards.
|
|
* Looking at you, HTMLCollection!
|
|
*
|
|
* @param {HTMLElement} recipient
|
|
* @param {Any} donor
|
|
* @param {Boolean} [noOverride]
|
|
* @return {HTMLElement}
|
|
*/
|
|
public proliferateElement(recipient: HTMLElement, donor: any, noOverride: boolean = false): HTMLElement {
|
|
var setting: any,
|
|
i: string,
|
|
j: number;
|
|
|
|
// For each attribute of the donor:
|
|
for (i in donor) {
|
|
if (donor.hasOwnProperty(i)) {
|
|
// If noOverride, don't override already existing properties
|
|
if (noOverride && recipient.hasOwnProperty(i)) {
|
|
continue;
|
|
}
|
|
|
|
setting = donor[i];
|
|
|
|
// Special cases for HTML elements
|
|
switch (i) {
|
|
// Children: just append all of them directly
|
|
case "children":
|
|
if (typeof (setting) !== "undefined") {
|
|
for (j = 0; j < setting.length; j += 1) {
|
|
recipient.appendChild(setting[j]);
|
|
}
|
|
}
|
|
break;
|
|
|
|
// Style: proliferate (instead of making a new Object)
|
|
case "style":
|
|
this.proliferateElement(recipient[i], setting);
|
|
break;
|
|
|
|
// By default, use the normal proliferate logic
|
|
default:
|
|
// If it's null, don't do anything (like .textContent)
|
|
if (setting === null) {
|
|
recipient[i] = null;
|
|
} else if (typeof setting === "object") {
|
|
// If it's an object, recurse on a new version of it
|
|
if (!recipient.hasOwnProperty(i)) {
|
|
recipient[i] = new setting.constructor();
|
|
}
|
|
this.proliferateElement(recipient[i], setting, noOverride);
|
|
} else {
|
|
// Regular primitives are easy to copy otherwise
|
|
recipient[i] = setting;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return recipient;
|
|
}
|
|
|
|
/**
|
|
* Resets the container elements. In any inherited resetElement, this should
|
|
* still be called, as it implements the schema's position.
|
|
*
|
|
* @param {Object} styles Container styles for the contained elements.
|
|
*/
|
|
protected resetElement(styles: IRootControlStyles, customType?: string): void {
|
|
var position: IPosition = this.schema.position,
|
|
offset: any = position.offset;
|
|
|
|
this.element = this.createElement("div", {
|
|
"className": "control",
|
|
"style": {
|
|
"position": "absolute",
|
|
"width": 0,
|
|
"height": 0,
|
|
"boxSizing": "border-box",
|
|
"opacity": ".84"
|
|
}
|
|
});
|
|
this.elementInner = this.createElement("div", {
|
|
"className": "control-inner",
|
|
"textContent": this.schema.label || "",
|
|
"style": {
|
|
"position": "absolute",
|
|
"boxSizing": "border-box",
|
|
"textAlign": "center"
|
|
}
|
|
});
|
|
this.element.appendChild(this.elementInner);
|
|
|
|
if (position.horizontal === "left") {
|
|
this.element.style.left = "0";
|
|
} else if (position.horizontal === "right") {
|
|
this.element.style.right = "0";
|
|
} else if (position.horizontal === "center") {
|
|
this.element.style.left = "50%";
|
|
}
|
|
|
|
if (position.vertical === "top") {
|
|
this.element.style.top = "0";
|
|
} else if (position.vertical === "bottom") {
|
|
this.element.style.bottom = "0";
|
|
} else if (position.vertical === "center") {
|
|
this.element.style.top = "50%";
|
|
}
|
|
|
|
this.passElementStyles(styles.global);
|
|
this.passElementStyles(styles[customType]);
|
|
this.passElementStyles(this.schema.styles);
|
|
|
|
if (offset.left) {
|
|
this.elementInner.style.marginLeft = this.createPixelMeasurement(offset.left);
|
|
}
|
|
|
|
if (offset.top) {
|
|
this.elementInner.style.marginTop = this.createPixelMeasurement(offset.top);
|
|
}
|
|
|
|
// elementInner's center-based positioning must wait until its total width is done setting
|
|
setTimeout(function (): void {
|
|
if (position.horizontal === "center") {
|
|
this.elementInner.style.left = this.createHalfSizeMeasurement(this.elementInner, "width", "offsetWidth");
|
|
}
|
|
if (position.vertical === "center") {
|
|
this.elementInner.style.top = this.createHalfSizeMeasurement(this.elementInner, "height", "offsetHeight");
|
|
}
|
|
}.bind(this));
|
|
}
|
|
|
|
/**
|
|
* Converts a String or Number into a CSS-ready String measurement.
|
|
*
|
|
* @param {Mixed} raw A raw measurement, such as "7" or "7px" or "7em".
|
|
* @return {String} The raw measurement as a CSS measurement.
|
|
*/
|
|
protected createPixelMeasurement(raw: string | number): string {
|
|
if (!raw) {
|
|
return "0";
|
|
}
|
|
|
|
if (typeof raw === "number" || raw.constructor === Number) {
|
|
return raw + "px";
|
|
}
|
|
|
|
return <string>raw;
|
|
}
|
|
|
|
/**
|
|
* Determines a "half"-measurement that would center an element based on the
|
|
* specified units.
|
|
*
|
|
* @param {HTMLElement} element The element whose half-size should be computed.
|
|
* @param {String} styleTag The initial CSS measurement to check for, as "width"
|
|
* or "height".
|
|
* @param {String} attributeBackup A measurement to check for if the CSS size
|
|
* is falsy, as "offsetWidth" or "offsetHeight".
|
|
* @returns {String} A measurement equal to half the sytleTag/attributeBackup,
|
|
* such as "3.5em" or "10px".
|
|
*/
|
|
protected createHalfSizeMeasurement(element: HTMLElement, styleTag: string, attributeBackup: string): string {
|
|
var amountRaw: string,
|
|
amount: number,
|
|
units: string;
|
|
|
|
amountRaw = element.style[styleTag] || (attributeBackup && element[attributeBackup]);
|
|
if (!amountRaw) {
|
|
return "0px";
|
|
}
|
|
|
|
amount = Number(amountRaw.replace(/[^\d]/g, "")) || 0;
|
|
units = amountRaw.replace(/[\d]/g, "") || "px";
|
|
|
|
return Math.round(amount / -2) + units;
|
|
}
|
|
|
|
/**
|
|
* Passes a style schema to .element and .elementInner.
|
|
*
|
|
* @param {Object} styles A container for styles to apply.
|
|
*/
|
|
protected passElementStyles(styles: IControlStyles): void {
|
|
if (!styles) {
|
|
return;
|
|
}
|
|
|
|
if (styles.element) {
|
|
this.proliferateElement(this.element, styles.element);
|
|
}
|
|
|
|
if (styles.elementInner) {
|
|
this.proliferateElement(this.elementInner, styles.elementInner);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the rotation of an HTML element via CSS.
|
|
*
|
|
* @param {HTMLElement} element
|
|
* @param {Number} rotation
|
|
*/
|
|
protected setRotation(element: HTMLElement, rotation: number): void {
|
|
element.style.transform = "rotate(" + rotation + "deg)";
|
|
}
|
|
|
|
/**
|
|
* Finds the position offset of an element relative to the page, factoring in
|
|
* its parent elements' offsets recursively.
|
|
*
|
|
* @param {HTMLElement} element
|
|
* @return {Number[]} The left and top offset of the element, in px.
|
|
*/
|
|
protected getOffsets(element: HTMLElement): number[] {
|
|
var output: number[];
|
|
|
|
if (element.offsetParent && element !== element.offsetParent) {
|
|
output = this.getOffsets(<HTMLElement>element.offsetParent);
|
|
output[0] += element.offsetLeft;
|
|
output[1] += element.offsetTop;
|
|
} else {
|
|
output = [element.offsetLeft, element.offsetTop];
|
|
}
|
|
|
|
return output;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Simple button control. It activates its triggers when the users presses
|
|
* it or releases it, and contains a simple label.
|
|
*/
|
|
export class ButtonControl extends Control {
|
|
/**
|
|
* The governing schema for this button.
|
|
*/
|
|
protected schema: IButtonSchema;
|
|
|
|
/**
|
|
* Resets the elements by adding listeners for mouse and touch
|
|
* activation and deactivation events.
|
|
*
|
|
* @param {Object} styles Container styles for the contained elements.
|
|
*/
|
|
protected resetElement(styles: IRootControlStyles): void {
|
|
var onActivated: any = this.onEvent.bind(this, "activated"),
|
|
onDeactivated: any = this.onEvent.bind(this, "deactivated");
|
|
|
|
super.resetElement(styles, "Button");
|
|
|
|
this.element.addEventListener("mousedown", onActivated);
|
|
this.element.addEventListener("touchstart", onActivated);
|
|
|
|
this.element.addEventListener("mouseup", onDeactivated);
|
|
this.element.addEventListener("touchend", onDeactivated);
|
|
}
|
|
|
|
/**
|
|
* Reation callback for a triggered event.
|
|
*
|
|
* @param {String} which The pipe being activated, such as
|
|
* "activated" or "deactivated".
|
|
* @param {Event} event
|
|
*/
|
|
protected onEvent(which: string, event: Event): void {
|
|
var events: any = (<IButtonSchema>this.schema).pipes[which],
|
|
i: string,
|
|
j: number;
|
|
|
|
if (!events) {
|
|
return;
|
|
}
|
|
|
|
for (i in events) {
|
|
if (!events.hasOwnProperty(i)) {
|
|
continue;
|
|
}
|
|
|
|
for (j = 0; j < events[i].length; j += 1) {
|
|
this.InputWriter.callEvent(i, events[i][j], event);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Joystick control. An inner circle can be dragged to one of a number
|
|
* of directions to trigger pipes on and off.
|
|
*/
|
|
export class JoystickControl extends Control {
|
|
/**
|
|
* The governing schema for this joystick
|
|
*/
|
|
protected schema: IJoystickSchema;
|
|
|
|
/**
|
|
* The large inner circle that visually surrounds the ticks and other
|
|
* inner elements.
|
|
*/
|
|
protected elementCircle: HTMLDivElement;
|
|
|
|
/**
|
|
* The normally hidden tick to display a snapped direction.
|
|
*/
|
|
protected elementDragLine: HTMLDivElement;
|
|
|
|
/**
|
|
* The normally hidden circle that emulates the outer part of a joystick.
|
|
*/
|
|
protected elementDragShadow: HTMLDivElement;
|
|
|
|
/**
|
|
* Whether dragging is currently enabled, generally by the user starting
|
|
* an interation event with touch or a mouse.
|
|
*/
|
|
protected dragEnabled: boolean;
|
|
|
|
/**
|
|
* The currently snaped direction, if dragEnabled is true.
|
|
*/
|
|
protected currentDirection: IJoystickDirection;
|
|
|
|
/**
|
|
* Resets the element by creating a tick for each direction, along with
|
|
* the multiple circular elements with their triggers.
|
|
*
|
|
* @param {Object} styles Container styles for the contained elements.
|
|
*/
|
|
protected resetElement(styles: IRootControlStyles): void {
|
|
super.resetElement(styles, "Joystick");
|
|
|
|
var directions: IJoystickDirection[] = (<IJoystickSchema>this.schema).directions,
|
|
element: HTMLDivElement,
|
|
degrees: number,
|
|
sin: number,
|
|
cos: number,
|
|
dx: number,
|
|
dy: number,
|
|
i: number;
|
|
|
|
this.proliferateElement(this.elementInner, {
|
|
"style": {
|
|
"border-radius": "100%"
|
|
}
|
|
});
|
|
|
|
// The visible circle is what is actually visible to the user
|
|
this.elementCircle = <HTMLDivElement>this.createElement("div", {
|
|
"className": "control-inner control-joystick-circle",
|
|
"style": {
|
|
"position": "absolute",
|
|
"background": "red",
|
|
"borderRadius": "100%"
|
|
}
|
|
});
|
|
this.proliferateElement(this.elementCircle, styles.Joystick.circle);
|
|
|
|
// Each direction creates a "tick" element, like on a clock
|
|
for (i = 0; i < directions.length; i += 1) {
|
|
degrees = directions[i].degrees;
|
|
|
|
// sin and cos are an amount / 1 the tick is offset from the center
|
|
sin = Math.sin(degrees * Math.PI / 180);
|
|
cos = Math.cos(degrees * Math.PI / 180);
|
|
|
|
// dx and dy are measured as percent from the center, based on sin & cos
|
|
dx = cos * 50 + 50;
|
|
dy = sin * 50 + 50;
|
|
|
|
element = <HTMLDivElement>this.createElement("div", {
|
|
"className": "control-joystick-tick",
|
|
"style": {
|
|
"position": "absolute",
|
|
"left": dx + "%",
|
|
"top": dy + "%",
|
|
"marginLeft": (-cos * 5 - 5) + "px",
|
|
"marginTop": (-sin * 2 - 1) + "px"
|
|
}
|
|
});
|
|
|
|
this.proliferateElement(element, styles.Joystick.tick);
|
|
this.setRotation(element, degrees);
|
|
|
|
this.elementCircle.appendChild(element);
|
|
}
|
|
|
|
// In addition to the ticks, a drag element shows current direction
|
|
this.elementDragLine = <HTMLDivElement>this.createElement("div", {
|
|
"className": "control-joystick-drag-line",
|
|
"style": {
|
|
"position": "absolute",
|
|
"opacity": "0",
|
|
"top": ".77cm",
|
|
"left": ".77cm"
|
|
}
|
|
});
|
|
this.proliferateElement(this.elementDragLine, styles.Joystick.dragLine);
|
|
this.elementCircle.appendChild(this.elementDragLine);
|
|
|
|
// A shadow-like circle supports the drag effect
|
|
this.elementDragShadow = <HTMLDivElement>this.createElement("div", {
|
|
"className": "control-joystick-drag-shadow",
|
|
"style": {
|
|
"position": "absolute",
|
|
"opacity": "1",
|
|
"top": "14%",
|
|
"right": "14%",
|
|
"bottom": "14%",
|
|
"left": "14%",
|
|
"marginLeft": "0",
|
|
"marginTop": "0",
|
|
"borderRadius": "100%"
|
|
}
|
|
});
|
|
this.proliferateElement(this.elementDragShadow, styles.Joystick.dragShadow);
|
|
this.elementCircle.appendChild(this.elementDragShadow);
|
|
|
|
this.elementInner.appendChild(this.elementCircle);
|
|
|
|
this.elementInner.addEventListener("click", this.triggerDragger.bind(this));
|
|
this.elementInner.addEventListener("touchmove", this.triggerDragger.bind(this));
|
|
this.elementInner.addEventListener("mousemove", this.triggerDragger.bind(this));
|
|
|
|
this.elementInner.addEventListener("mouseover", this.positionDraggerEnable.bind(this));
|
|
this.elementInner.addEventListener("touchstart", this.positionDraggerEnable.bind(this));
|
|
|
|
this.elementInner.addEventListener("mouseout", this.positionDraggerDisable.bind(this));
|
|
this.elementInner.addEventListener("touchend", this.positionDraggerDisable.bind(this));
|
|
}
|
|
|
|
/**
|
|
* Enables dragging, showing the elementDragLine.
|
|
*/
|
|
protected positionDraggerEnable(): void {
|
|
this.dragEnabled = true;
|
|
this.elementDragLine.style.opacity = "1";
|
|
}
|
|
|
|
/**
|
|
* Disables dragging, hiding the drag line and re-centering the
|
|
* inner circle shadow.
|
|
*/
|
|
protected positionDraggerDisable(): void {
|
|
this.dragEnabled = false;
|
|
this.elementDragLine.style.opacity = "0";
|
|
|
|
this.elementDragShadow.style.top = "14%";
|
|
this.elementDragShadow.style.right = "14%";
|
|
this.elementDragShadow.style.bottom = "14%";
|
|
this.elementDragShadow.style.left = "14%";
|
|
|
|
if (this.currentDirection) {
|
|
if (this.currentDirection.pipes && this.currentDirection.pipes.deactivated) {
|
|
this.onEvent(this.currentDirection.pipes.deactivated, event);
|
|
}
|
|
|
|
this.currentDirection = undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Triggers a movement point for the joystick, and snaps the stick to
|
|
* the nearest direction (based on the angle from the center to the point).
|
|
*
|
|
* @param {Event} event
|
|
*/
|
|
protected triggerDragger(event: DragEvent | MouseEvent): void {
|
|
event.preventDefault();
|
|
|
|
if (!this.dragEnabled) {
|
|
return;
|
|
}
|
|
|
|
var coordinates: number[] = this.getEventCoordinates(event),
|
|
x: number = coordinates[0],
|
|
y: number = coordinates[1],
|
|
offsets: number[] = this.getOffsets(this.elementInner),
|
|
midX: number = offsets[0] + this.elementInner.offsetWidth / 2,
|
|
midY: number = offsets[1] + this.elementInner.offsetHeight / 2,
|
|
dxRaw: number = (x - midX) | 0,
|
|
dyRaw: number = (midY - y) | 0,
|
|
thetaRaw: number = this.getThetaRaw(dxRaw, dyRaw),
|
|
directionNumber: number = this.findClosestDirection(thetaRaw),
|
|
direction: IJoystickDirection = (<IJoystickSchema>this.schema).directions[directionNumber],
|
|
theta: number = direction.degrees,
|
|
components: number[] = this.getThetaComponents(theta),
|
|
dx: number = components[0],
|
|
dy: number = -components[1];
|
|
|
|
this.proliferateElement(this.elementDragLine, {
|
|
"style": {
|
|
"marginLeft": ((dx * 77) | 0) + "%",
|
|
"marginTop": ((dy * 77) | 0) + "%"
|
|
}
|
|
});
|
|
|
|
this.proliferateElement(this.elementDragShadow, {
|
|
"style": {
|
|
"top": ((14 + dy * 10) | 0) + "%",
|
|
"right": ((14 - dx * 10) | 0) + "%",
|
|
"bottom": ((14 - dy * 10) | 0) + "%",
|
|
"left": ((14 + dx * 10) | 0) + "%"
|
|
}
|
|
});
|
|
|
|
// Ensure theta is above 0, and offset it by 90 for visual rotation
|
|
theta = (theta + 450) % 360;
|
|
|
|
this.setRotation(this.elementDragLine, theta);
|
|
this.positionDraggerEnable();
|
|
|
|
this.setCurrentDirection(direction, event);
|
|
}
|
|
|
|
/**
|
|
* Finds the raw coordinates of an event, whether it's a drag (touch)
|
|
* or mouse event.
|
|
*
|
|
* @return {Number[]} The x- and y- coordinates of the event.
|
|
*/
|
|
protected getEventCoordinates(event: DragEvent | MouseEvent): number[] {
|
|
if (event.type === "touchmove") {
|
|
// TypeScript 1.5 doesn't seem to have TouchEvent yet.
|
|
var touch: any = (<any>event).touches[0];
|
|
return [touch.pageX, touch.pageY];
|
|
}
|
|
|
|
return [(<MouseEvent>event).x, (<MouseEvent>event).y];
|
|
}
|
|
|
|
/**
|
|
* Finds the angle from a joystick center to an x and y. This assumes
|
|
* straight up is 0, to the right is 90, down is 180, and left is 270.
|
|
*
|
|
* @return {Number} The degrees to the given point.
|
|
*/
|
|
protected getThetaRaw(dxRaw: number, dyRaw: number): number {
|
|
// Based on the quadrant, theta changes...
|
|
if (dxRaw > 0) {
|
|
if (dyRaw > 0) {
|
|
// Quadrant I
|
|
return Math.atan(dxRaw / dyRaw) * 180 / Math.PI;
|
|
} else {
|
|
// Quadrant II
|
|
return -Math.atan(dyRaw / dxRaw) * 180 / Math.PI + 90;
|
|
}
|
|
} else {
|
|
if (dyRaw < 0) {
|
|
// Quadrant III
|
|
return Math.atan(dxRaw / dyRaw) * 180 / Math.PI + 180;
|
|
} else {
|
|
// Quadrant IV
|
|
return -Math.atan(dyRaw / dxRaw) * 180 / Math.PI + 270;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts an angle to its relative dx and dy coordinates.
|
|
*
|
|
* @param {Number} thetaRaw
|
|
* @return {Number[]} The x- and y- parts of an angle.
|
|
*/
|
|
protected getThetaComponents(thetaRaw: number): number[] {
|
|
var theta: number = thetaRaw * Math.PI / 180;
|
|
return [Math.sin(theta), Math.cos(theta)];
|
|
}
|
|
|
|
/**
|
|
* Finds the index of the closest direction to an angle.
|
|
*
|
|
* @param {Number} degrees
|
|
* @return {Number}
|
|
*/
|
|
protected findClosestDirection(degrees: number): number {
|
|
var directions: IJoystickDirection[] = (<IJoystickSchema>this.schema).directions,
|
|
difference: number = Math.abs(directions[0].degrees - degrees),
|
|
smallestDegrees: number = directions[0].degrees,
|
|
smallestDegreesRecord: number = 0,
|
|
record: number = 0,
|
|
differenceTest: number,
|
|
i: number;
|
|
|
|
// Find the direction with the smallest difference in degrees
|
|
for (i = 1; i < directions.length; i += 1) {
|
|
differenceTest = Math.abs(directions[i].degrees - degrees);
|
|
if (differenceTest < difference) {
|
|
difference = differenceTest;
|
|
record = i;
|
|
}
|
|
if (directions[i].degrees < smallestDegrees) {
|
|
smallestDegrees = directions[i].degrees;
|
|
smallestDegreesRecord = i;
|
|
}
|
|
}
|
|
|
|
// 359 is closer to 360 than 0, so pretend the smallest is above 360
|
|
differenceTest = Math.abs(smallestDegrees + 360 - degrees);
|
|
if (differenceTest < difference) {
|
|
difference = differenceTest;
|
|
record = smallestDegreesRecord;
|
|
}
|
|
|
|
return record;
|
|
}
|
|
|
|
/**
|
|
* Sets the current direction of the joystick, calling the relevant
|
|
* InputWriter pipes if necessary.
|
|
*
|
|
* @param {Object} direction
|
|
* @param {Event} [event]
|
|
*/
|
|
protected setCurrentDirection(direction: IJoystickDirection, event?: Event): void {
|
|
if (this.currentDirection === direction) {
|
|
return;
|
|
}
|
|
|
|
if (this.currentDirection && this.currentDirection.pipes) {
|
|
if (this.currentDirection.pipes.deactivated) {
|
|
this.onEvent(this.currentDirection.pipes.deactivated, event);
|
|
}
|
|
}
|
|
|
|
if (direction.pipes && direction.pipes.activated) {
|
|
this.onEvent(direction.pipes.activated, event);
|
|
}
|
|
|
|
this.currentDirection = direction;
|
|
}
|
|
|
|
/**
|
|
* Trigger for calling pipes when a new direction is set. All children
|
|
* of the pipe has each of its keys triggered.
|
|
*
|
|
* @param {Object} pipes
|
|
* @param {Event} [event]
|
|
*/
|
|
protected onEvent(pipes: IPipes, event?: Event): void {
|
|
var i: string,
|
|
j: number;
|
|
|
|
for (i in pipes) {
|
|
if (!pipes.hasOwnProperty(i)) {
|
|
continue;
|
|
}
|
|
|
|
for (j = 0; j < pipes[i].length; j += 1) {
|
|
this.InputWriter.callEvent(i, pipes[i][j], event);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
export class TouchPassr implements ITouchPassr {
|
|
/**
|
|
* An InputWritr for controls to pipe event triggers to.
|
|
*/
|
|
private InputWriter: InputWritr.IInputWritr;
|
|
|
|
/**
|
|
* Whether this is currently enabled and visually on the screen.
|
|
*/
|
|
private enabled: boolean;
|
|
|
|
/**
|
|
* Root container for styles to be added to control elements.
|
|
*/
|
|
private styles: IRootControlStyles;
|
|
|
|
/**
|
|
* Container for generated controls, keyed by their name.
|
|
*/
|
|
private controls: IControlsContainer;
|
|
|
|
/**
|
|
* HTMLElement all controls are placed within.
|
|
*/
|
|
private container: HTMLElement;
|
|
|
|
/**
|
|
* HTMLElement containing the controls container.
|
|
*/
|
|
private parentContainer: HTMLElement;
|
|
|
|
/**
|
|
* @param {ITouchPassrSettings} settings
|
|
*/
|
|
constructor(settings: ITouchPassrSettings) {
|
|
if (typeof settings === "undefined") {
|
|
throw new Error("No settings object given to TouchPassr.");
|
|
}
|
|
if (typeof settings.InputWriter === "undefined") {
|
|
throw new Error("No InputWriter given to TouchPassr.");
|
|
}
|
|
|
|
this.InputWriter = settings.InputWriter;
|
|
this.styles = settings.styles || {};
|
|
|
|
this.resetContainer(settings.container);
|
|
|
|
this.controls = {};
|
|
if (settings.controls) {
|
|
this.addControls(settings.controls);
|
|
}
|
|
|
|
if (typeof settings.enabled === "undefined") {
|
|
this.enabled = true;
|
|
} else {
|
|
this.enabled = settings.enabled;
|
|
}
|
|
|
|
this.enabled ? this.enable() : this.disable();
|
|
}
|
|
|
|
|
|
/* Simple gets
|
|
*/
|
|
|
|
/**
|
|
* @return {InputWritr} The InputWritr for controls to pipe event triggers to.
|
|
*/
|
|
getInputWriter(): InputWritr.IInputWritr {
|
|
return this.InputWriter;
|
|
}
|
|
|
|
/**
|
|
* @return {Boolean} Whether this is currently enabled and visually on the screen.
|
|
*/
|
|
getEnabled(): boolean {
|
|
return this.enabled;
|
|
}
|
|
|
|
/**
|
|
* @return {Object} The root container for styles to be added to control elements.
|
|
*/
|
|
getStyles(): IRootControlStyles {
|
|
return this.styles;
|
|
}
|
|
|
|
/**
|
|
* @return {Object} The container for generated controls, keyed by their name.
|
|
*/
|
|
getControls(): IControlsContainer {
|
|
return this.controls;
|
|
}
|
|
|
|
/**
|
|
* @return {HTMLElement} The HTMLElement all controls are placed within.
|
|
*/
|
|
getContainer(): HTMLElement {
|
|
return this.container;
|
|
}
|
|
|
|
/**
|
|
* @return {HTMLElement} The HTMLElement containing the controls container.
|
|
*/
|
|
getParentContainer(): HTMLElement {
|
|
return this.parentContainer;
|
|
}
|
|
|
|
|
|
/* Core functionality
|
|
*/
|
|
|
|
/**
|
|
* Enables the TouchPassr by showing the container.
|
|
*/
|
|
enable(): void {
|
|
this.enabled = true;
|
|
this.container.style.display = "block";
|
|
}
|
|
|
|
/**
|
|
* Disables the TouchPassr by hiding the container.
|
|
*/
|
|
disable(): void {
|
|
this.enabled = false;
|
|
this.container.style.display = "none";
|
|
}
|
|
|
|
/**
|
|
* Sets the parent container surrounding the controls container.
|
|
*
|
|
* @param {HTMLElement} parentElement
|
|
*/
|
|
setParentContainer(parentElement: HTMLElement): void {
|
|
this.parentContainer = parentElement;
|
|
this.parentContainer.appendChild(this.container);
|
|
}
|
|
|
|
/**
|
|
* Adds any number of controls to the internal listing and HTML container.
|
|
*
|
|
* @param {Object} schemas Schemas for new controls to be made, keyed by name.
|
|
*/
|
|
addControls(schemas: IControlSchemasContainer): void {
|
|
var i: string;
|
|
|
|
for (i in schemas) {
|
|
if (schemas.hasOwnProperty(i)) {
|
|
this.addControl(schemas[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds a control to the internal listing and HTML container.
|
|
*
|
|
* @param {Object} schema The schema for the new control to be made.
|
|
*/
|
|
addControl(schema: IControlSchema): void {
|
|
var control: Control;
|
|
|
|
switch (schema.control) {
|
|
case "Button":
|
|
control = new ButtonControl(this.InputWriter, schema, this.styles);
|
|
break;
|
|
|
|
case "Joystick":
|
|
control = new JoystickControl(this.InputWriter, schema, this.styles);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
this.controls[schema.name] = control;
|
|
this.container.appendChild(control.getElement());
|
|
}
|
|
|
|
|
|
/* HTML manipulations
|
|
*/
|
|
|
|
/**
|
|
* Resets the base controls container. If a parent element is provided,
|
|
* the container is added to it.
|
|
*
|
|
* @param {HTMLElement} [parentContainer] A container element, such as
|
|
* from GameStartr.
|
|
*/
|
|
private resetContainer(parentContainer?: HTMLElement): void {
|
|
this.container = Control.prototype.createElement("div", {
|
|
"className": "touch-passer-container",
|
|
"style": {
|
|
"position": "absolute",
|
|
"top": 0,
|
|
"right": 0,
|
|
"bottom": 0,
|
|
"left": 0
|
|
}
|
|
});
|
|
|
|
if (parentContainer) {
|
|
this.setParentContainer(parentContainer);
|
|
}
|
|
}
|
|
}
|
|
}
|