1
0
forked from sent/waves
waves/public/assets/g/mario/References/TouchPassr-0.2.0.js
2025-04-09 17:11:14 -05:00

742 lines
31 KiB
JavaScript

/// <reference path="InputWritr-0.2.0.ts" />
var __extends = (this && this.__extends) || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
__.prototype = b.prototype;
d.prototype = new __();
};
var TouchPassr;
(function (TouchPassr_1) {
"use strict";
/**
* Abstract class for on-screen controls. Element creation for .element
* and .elementInner within the constrained position is provided.
*/
var Control = (function () {
/**
* Resets the control by setting member variables and calling resetElement.
*
* @param {InputWritr} InputWriter
* @param {Object} schema
*/
function Control(InputWriter, schema, styles) {
this.InputWriter = InputWriter;
this.schema = schema;
this.resetElement(styles);
}
/**
* @return {HTMLElement} The outer container element.
*/
Control.prototype.getElement = function () {
return this.element;
};
/**
* @return {HTMLElement} The inner container element.
*/
Control.prototype.getElementInner = function () {
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}
*/
Control.prototype.createElement = function (tag) {
var args = [];
for (var _i = 1; _i < arguments.length; _i++) {
args[_i - 1] = arguments[_i];
}
var element = document.createElement(tag || "div"), i;
// 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}
*/
Control.prototype.proliferateElement = function (recipient, donor, noOverride) {
if (noOverride === void 0) { noOverride = false; }
var setting, i, j;
// 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.
*/
Control.prototype.resetElement = function (styles, customType) {
var position = this.schema.position, offset = 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 () {
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.
*/
Control.prototype.createPixelMeasurement = function (raw) {
if (!raw) {
return "0";
}
if (typeof raw === "number" || raw.constructor === Number) {
return raw + "px";
}
return 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".
*/
Control.prototype.createHalfSizeMeasurement = function (element, styleTag, attributeBackup) {
var amountRaw, amount, units;
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.
*/
Control.prototype.passElementStyles = function (styles) {
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
*/
Control.prototype.setRotation = function (element, rotation) {
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.
*/
Control.prototype.getOffsets = function (element) {
var output;
if (element.offsetParent && element !== element.offsetParent) {
output = this.getOffsets(element.offsetParent);
output[0] += element.offsetLeft;
output[1] += element.offsetTop;
}
else {
output = [element.offsetLeft, element.offsetTop];
}
return output;
};
return Control;
})();
TouchPassr_1.Control = Control;
/**
* Simple button control. It activates its triggers when the users presses
* it or releases it, and contains a simple label.
*/
var ButtonControl = (function (_super) {
__extends(ButtonControl, _super);
function ButtonControl() {
_super.apply(this, arguments);
}
/**
* Resets the elements by adding listeners for mouse and touch
* activation and deactivation events.
*
* @param {Object} styles Container styles for the contained elements.
*/
ButtonControl.prototype.resetElement = function (styles) {
var onActivated = this.onEvent.bind(this, "activated"), onDeactivated = this.onEvent.bind(this, "deactivated");
_super.prototype.resetElement.call(this, 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
*/
ButtonControl.prototype.onEvent = function (which, event) {
var events = this.schema.pipes[which], i, j;
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);
}
}
};
return ButtonControl;
})(Control);
TouchPassr_1.ButtonControl = ButtonControl;
/**
* Joystick control. An inner circle can be dragged to one of a number
* of directions to trigger pipes on and off.
*/
var JoystickControl = (function (_super) {
__extends(JoystickControl, _super);
function JoystickControl() {
_super.apply(this, arguments);
}
/**
* 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.
*/
JoystickControl.prototype.resetElement = function (styles) {
_super.prototype.resetElement.call(this, styles, "Joystick");
var directions = this.schema.directions, element, degrees, sin, cos, dx, dy, i;
this.proliferateElement(this.elementInner, {
"style": {
"border-radius": "100%"
}
});
// The visible circle is what is actually visible to the user
this.elementCircle = 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 = 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 = 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 = 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.
*/
JoystickControl.prototype.positionDraggerEnable = function () {
this.dragEnabled = true;
this.elementDragLine.style.opacity = "1";
};
/**
* Disables dragging, hiding the drag line and re-centering the
* inner circle shadow.
*/
JoystickControl.prototype.positionDraggerDisable = function () {
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
*/
JoystickControl.prototype.triggerDragger = function (event) {
event.preventDefault();
if (!this.dragEnabled) {
return;
}
var coordinates = this.getEventCoordinates(event), x = coordinates[0], y = coordinates[1], offsets = this.getOffsets(this.elementInner), midX = offsets[0] + this.elementInner.offsetWidth / 2, midY = offsets[1] + this.elementInner.offsetHeight / 2, dxRaw = (x - midX) | 0, dyRaw = (midY - y) | 0, thetaRaw = this.getThetaRaw(dxRaw, dyRaw), directionNumber = this.findClosestDirection(thetaRaw), direction = this.schema.directions[directionNumber], theta = direction.degrees, components = this.getThetaComponents(theta), dx = components[0], dy = -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.
*/
JoystickControl.prototype.getEventCoordinates = function (event) {
if (event.type === "touchmove") {
// TypeScript 1.5 doesn't seem to have TouchEvent yet.
var touch = event.touches[0];
return [touch.pageX, touch.pageY];
}
return [event.x, 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.
*/
JoystickControl.prototype.getThetaRaw = function (dxRaw, dyRaw) {
// 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.
*/
JoystickControl.prototype.getThetaComponents = function (thetaRaw) {
var theta = 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}
*/
JoystickControl.prototype.findClosestDirection = function (degrees) {
var directions = this.schema.directions, difference = Math.abs(directions[0].degrees - degrees), smallestDegrees = directions[0].degrees, smallestDegreesRecord = 0, record = 0, differenceTest, i;
// 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]
*/
JoystickControl.prototype.setCurrentDirection = function (direction, event) {
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]
*/
JoystickControl.prototype.onEvent = function (pipes, event) {
var i, j;
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);
}
}
};
return JoystickControl;
})(Control);
TouchPassr_1.JoystickControl = JoystickControl;
/**
*
*/
var TouchPassr = (function () {
/**
* @param {ITouchPassrSettings} settings
*/
function TouchPassr(settings) {
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.
*/
TouchPassr.prototype.getInputWriter = function () {
return this.InputWriter;
};
/**
* @return {Boolean} Whether this is currently enabled and visually on the screen.
*/
TouchPassr.prototype.getEnabled = function () {
return this.enabled;
};
/**
* @return {Object} The root container for styles to be added to control elements.
*/
TouchPassr.prototype.getStyles = function () {
return this.styles;
};
/**
* @return {Object} The container for generated controls, keyed by their name.
*/
TouchPassr.prototype.getControls = function () {
return this.controls;
};
/**
* @return {HTMLElement} The HTMLElement all controls are placed within.
*/
TouchPassr.prototype.getContainer = function () {
return this.container;
};
/**
* @return {HTMLElement} The HTMLElement containing the controls container.
*/
TouchPassr.prototype.getParentContainer = function () {
return this.parentContainer;
};
/* Core functionality
*/
/**
* Enables the TouchPassr by showing the container.
*/
TouchPassr.prototype.enable = function () {
this.enabled = true;
this.container.style.display = "block";
};
/**
* Disables the TouchPassr by hiding the container.
*/
TouchPassr.prototype.disable = function () {
this.enabled = false;
this.container.style.display = "none";
};
/**
* Sets the parent container surrounding the controls container.
*
* @param {HTMLElement} parentElement
*/
TouchPassr.prototype.setParentContainer = function (parentElement) {
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.
*/
TouchPassr.prototype.addControls = function (schemas) {
var i;
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.
*/
TouchPassr.prototype.addControl = function (schema) {
var 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.
*/
TouchPassr.prototype.resetContainer = function (parentContainer) {
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);
}
};
return TouchPassr;
})();
TouchPassr_1.TouchPassr = TouchPassr;
})(TouchPassr || (TouchPassr = {}));