diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a261f29 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dist/* diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..d8cb142 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1 @@ +export * from './joy'; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..d8cb142 --- /dev/null +++ b/dist/index.js @@ -0,0 +1 @@ +export * from './joy'; diff --git a/dist/joy.d.ts b/dist/joy.d.ts new file mode 100644 index 0000000..cbc9656 --- /dev/null +++ b/dist/joy.d.ts @@ -0,0 +1,121 @@ +export interface StickStatus { + xPosition: number; + yPosition: number; + x: number; + y: number; + cardinalDirection: CardinalDirection; +} +export type CardinalDirection = "C" | "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW"; +interface JoyStickMembers { + title: string; + width: number; + height: number; + internalFillColor: string; + internalLineWidth: number; + internalStrokeColor: string; + externalLineWidth: number; + externalStrokeColor: string; + autoReturnToCenter: boolean; + context: CanvasRenderingContext2D; + canvas: HTMLCanvasElement; + circumference: number; + internalRadius: number; + maxMoveStick: number; + xCenter: number; + yCenter: number; + movedX: number; + movedY: number; + pressed: boolean; + callback: JoyStickCallback; + stickStatus: StickStatus; +} +export interface JoyStickParameters extends Partial { +} +export type JoyStickCallback = (stickStatus: StickStatus) => void; +export declare class JoyStick { + private title; + private width; + private height; + private internalFillColor; + private internalLineWidth; + private internalStrokeColor; + private externalLineWidth; + private externalStrokeColor; + private autoReturnToCenter; + private callback; + private objContainer; + private context; + private canvas; + private circumference; + private internalRadius; + private externalRadius; + private maxMoveStick; + private centerX; + private centerY; + private directionVerticalLimitPos; + private directionVerticalLimitNeg; + private directionHorizontalLimitPos; + private directionHorizontalLimitNeg; + private pressed; + private movedX; + private movedY; + private stickStatus; + constructor(containerId: string, parameters?: JoyStickParameters, callback?: JoyStickCallback); + /** + * @desc Draw the external circle used as reference position + */ + private drawExternal; + /** + * @desc Draw the internal stick in the current position the user have moved it + */ + private drawInternal; + /** + * @desc Events for manage touch + */ + private onTouchStart; + private onTouchMove; + private onTouchEnd; + /** + * @desc Events for manage mouse + */ + private onMouseDown; + private onMouseMove; + private onMouseUp; + getCardinalDirection(): CardinalDirection; + /** + * @desc The width of canvas + * @return Number of pixel width + */ + getWidth(): number; + /** + * @desc The height of canvas + * @return Number of pixel height + */ + getHeigh(): number; + /** + * @desc The X position of the cursor relative to the canvas that contains it and to its dimensions + * @return Number that indicate relative position + */ + getPosX(): number; + /** + * @desc The Y position of the cursor relative to the canvas that contains it and to its dimensions + * @return Number that indicate relative position + */ + getPosY(): number; + /** + * @desc Normalizzed value of X move of stick + * @return Integer from -100 to +100 + */ + getX(): number; + /** + * @desc Normalizzed value of Y move of stick + * @return Integer from -100 to +100 + */ + getY(): number; + /** + * @desc Get the direction of the cursor as a string that indicates the cardinal points where this is oriented + * @return String of cardinal point N, NE, E, SE, S, SW, W, NW and C when it is placed in the center + */ + getDir(): CardinalDirection; +} +export default JoyStick; diff --git a/dist/joy.js b/dist/joy.js new file mode 100644 index 0000000..e408d8d --- /dev/null +++ b/dist/joy.js @@ -0,0 +1,380 @@ +/* + * Name : joy.js + * @author : Roberto D'Amico (Bobboteck) + * Last modified : 09.22.2023 + * Revision : 1.1.6 + * + * Modification History: + * Date Version Modified By Description + * 2023-09-22 3.0.0 cybaj Porting to TypeScript + * 2021-12-21 2.0.0 Roberto D'Amico New version of the project that integrates the callback functions, while + * maintaining compatibility with previous versions. Fixed Issue #27 too, + * thanks to @artisticfox8 for the suggestion. + * 2020-06-09 1.1.6 Roberto D'Amico Fixed Issue #10 and #11 + * 2020-04-20 1.1.5 Roberto D'Amico Correct: Two sticks in a row, thanks to @liamw9534 for the suggestion + * 2020-04-03 Roberto D'Amico Correct: InternalRadius when change the size of canvas, thanks to + * @vanslipon for the suggestion + * 2020-01-07 1.1.4 Roberto D'Amico Close #6 by implementing a new parameter to set the functionality of + * auto-return to 0 position + * 2019-11-18 1.1.3 Roberto D'Amico Close #5 correct indication of East direction + * 2019-11-12 1.1.2 Roberto D'Amico Removed Fix #4 incorrectly introduced and restored operation with touch + * devices + * 2019-11-12 1.1.1 Roberto D'Amico Fixed Issue #4 - Now JoyStick work in any position in the page, not only + * at 0,0 + * + * The MIT License (MIT) + * + * This file is part of the JoyStick Project (https://github.com/bobboteck/JoyStick). + * Copyright (c) 2015 Roberto D'Amico (Bobboteck). + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +const defaultStickStatus = { + xPosition: 0, + yPosition: 0, + x: 0, + y: 0, + cardinalDirection: "C", +}; +const JoyStickDefaultParameters = { + title: "joystick", + width: 0, + height: 0, + internalFillColor: "#00AA00", + internalLineWidth: 2, + internalStrokeColor: "#003300", + externalLineWidth: 2, + externalStrokeColor: "#008000", + autoReturnToCenter: true, +}; +export class JoyStick { + constructor(containerId, parameters = {}, callback = () => { }) { + this.callback = callback; + // Merge the user parameters with the default ones + // and check if the mandatory parameters are defined + // otherwise throw an error. + const mergedParameters = Object.assign(Object.assign({}, JoyStickDefaultParameters), parameters); + this.title = mergedParameters.title; + this.width = mergedParameters.width; + this.height = mergedParameters.height; + this.internalFillColor = mergedParameters.internalFillColor; + this.internalLineWidth = mergedParameters.internalLineWidth; + this.internalStrokeColor = mergedParameters.internalStrokeColor; + this.externalLineWidth = mergedParameters.externalLineWidth; + this.externalStrokeColor = mergedParameters.externalStrokeColor; + this.autoReturnToCenter = mergedParameters.autoReturnToCenter; + this.objContainer = document.getElementById(containerId); + // Fixing Unable to preventDefault inside passive event listener due to target being treated as passive in Chrome [Thanks to https://github.com/artisticfox8 for this suggestion] + if (!this.objContainer) { + throw new Error(`The container with id '${containerId}' was not found in the DOM.`); + } + this.objContainer.style.touchAction = "none"; + this.canvas = document.createElement("canvas"); + this.canvas.id = this.title; + if (this.width === 0) { + this.width = this.objContainer.clientWidth; + } + if (this.height === 0) { + this.height = this.objContainer.clientHeight; + } + this.canvas.width = this.width; + this.canvas.height = this.height; + this.objContainer.appendChild(this.canvas); + const context = this.canvas.getContext("2d"); + if (!context) { + throw new Error("Unable to get the 2D context from the canvas."); + } + this.context = context; + // configuration of the Joystick + this.circumference = 2 * Math.PI; + this.internalRadius = + (this.canvas.width - (this.canvas.width / 2 + 10)) / 2; + this.maxMoveStick = this.internalRadius + 5; + this.externalRadius = this.internalRadius + 30; + this.centerX = this.canvas.width / 2; + this.centerY = this.canvas.height / 2; + this.directionHorizontalLimitPos = this.canvas.width / 10; + this.directionHorizontalLimitNeg = this.directionHorizontalLimitPos * -1; + this.directionVerticalLimitPos = this.canvas.height / 10; + this.directionVerticalLimitNeg = this.directionVerticalLimitPos * -1; + this.stickStatus = defaultStickStatus; + this.pressed = false; + // Used to save current position of stick + this.movedX = this.centerX; + this.movedY = this.centerY; + // Check if the device support the touch or not + if ("ontouchstart" in document.documentElement) { + this.canvas.addEventListener("touchstart", this.onTouchStart.bind(this), false); + document.addEventListener("touchmove", this.onTouchMove.bind(this), false); + document.addEventListener("touchend", this.onTouchEnd.bind(this), false); + } + else { + this.canvas.addEventListener("mousedown", this.onMouseDown.bind(this), false); + document.addEventListener("mousemove", this.onMouseMove.bind(this), false); + document.addEventListener("mouseup", this.onMouseUp.bind(this), false); + } + // Draw the object + this.drawExternal(); + this.drawInternal(); + } + /** + * @desc Draw the external circle used as reference position + */ + drawExternal() { + this.context.beginPath(); + this.context.arc(this.centerX, this.centerY, this.externalRadius, 0, this.circumference, false); + this.context.lineWidth = this.externalLineWidth; + this.context.strokeStyle = this.externalStrokeColor; + this.context.stroke(); + } + /** + * @desc Draw the internal stick in the current position the user have moved it + */ + drawInternal() { + this.context.beginPath(); + if (this.movedX < this.internalRadius) { + this.movedX = this.maxMoveStick; + } + if (this.movedX + this.internalRadius > this.canvas.width) { + this.movedX = this.canvas.width - this.maxMoveStick; + } + if (this.movedY < this.internalRadius) { + this.movedY = this.maxMoveStick; + } + if (this.movedY + this.internalRadius > this.canvas.height) { + this.movedY = this.canvas.height - this.maxMoveStick; + } + this.context.arc(this.movedX, this.movedY, this.internalRadius, 0, this.circumference, false); + // create radial gradient + var grd = this.context.createRadialGradient(this.centerX, this.centerY, 5, this.centerX, this.centerY, 200); + // Light color + grd.addColorStop(0, this.internalFillColor); + // Dark color + grd.addColorStop(1, this.internalStrokeColor); + this.context.fillStyle = grd; + this.context.fill(); + this.context.lineWidth = this.internalLineWidth; + this.context.strokeStyle = this.internalStrokeColor; + this.context.stroke(); + } + /** + * @desc Events for manage touch + */ + onTouchStart(event) { + this.pressed = true; + } + onTouchMove(event) { + var _a, _b, _c; + if (this.pressed && event.targetTouches[0].target === this.canvas) { + this.movedX = event.targetTouches[0].pageX; + this.movedY = event.targetTouches[0].pageY; + // Manage offset + if (((_a = this.canvas.offsetParent) === null || _a === void 0 ? void 0 : _a.tagName.toUpperCase()) === "BODY") { + this.movedX -= this.canvas.offsetLeft; + this.movedY -= this.canvas.offsetTop; + } + else { + // TODO delete offsetParent dependency + // @ts-ignore + this.movedX -= (_b = this.canvas.offsetParent) === null || _b === void 0 ? void 0 : _b.offsetLeft; + // @ts-ignore + this.movedY -= (_c = this.canvas.offsetParent) === null || _c === void 0 ? void 0 : _c.offsetTop; + } + // Delete canvas + this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); + // Redraw object + this.drawExternal(); + this.drawInternal(); + // Set attribute of callback + this.stickStatus.xPosition = this.movedX; + this.stickStatus.yPosition = this.movedY; + this.stickStatus.x = + 100 * ((this.movedX - this.centerX) / this.maxMoveStick); + this.stickStatus.y = + 100 * ((this.movedY - this.centerY) / this.maxMoveStick) * -1; + this.stickStatus.cardinalDirection = this.getCardinalDirection(); + this.callback(this.stickStatus); + } + } + onTouchEnd(event) { + this.pressed = false; + // If required reset position store variable + if (this.autoReturnToCenter) { + this.movedX = this.centerX; + this.movedY = this.centerY; + } + // Delete canvas + this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); + // Redraw object + this.drawExternal(); + this.drawInternal(); + // Set attribute of callback + this.stickStatus.xPosition = this.movedX; + this.stickStatus.yPosition = this.movedY; + this.stickStatus.x = + 100 * ((this.movedX - this.centerX) / this.maxMoveStick); + this.stickStatus.y = + 100 * ((this.movedY - this.centerY) / this.maxMoveStick) * -1; + this.stickStatus.cardinalDirection = this.getCardinalDirection(); + this.callback(this.stickStatus); + } + /** + * @desc Events for manage mouse + */ + onMouseDown(event) { + this.pressed = true; + } + /* To simplify this code there was a new experimental feature here: https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/offsetX , but it present only in Mouse case not metod presents in Touch case :-( */ + onMouseMove(event) { + var _a, _b, _c; + if (this.pressed) { + this.movedX = event.pageX; + this.movedY = event.pageY; + // Manage offset + if (((_a = this.canvas.offsetParent) === null || _a === void 0 ? void 0 : _a.tagName.toUpperCase()) === "BODY") { + this.movedX -= this.canvas.offsetLeft; + this.movedY -= this.canvas.offsetTop; + } + else { + // @ts-ignore + this.movedX -= (_b = this.canvas.offsetParent) === null || _b === void 0 ? void 0 : _b.offsetLeft; + // @ts-ignore + this.movedY -= (_c = this.canvas.offsetParent) === null || _c === void 0 ? void 0 : _c.offsetTop; + } + // Delete canvas + this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); + // Redraw object + this.drawExternal(); + this.drawInternal(); + // Set attribute of callback + this.stickStatus.xPosition = this.movedX; + this.stickStatus.yPosition = this.movedY; + this.stickStatus.x = + 100 * ((this.movedX - this.centerX) / this.maxMoveStick); + this.stickStatus.y = + 100 * ((this.movedY - this.centerY) / this.maxMoveStick) * -1; + this.stickStatus.cardinalDirection = this.getCardinalDirection(); + this.callback(this.stickStatus); + } + } + onMouseUp(event) { + this.pressed = false; + // If required reset position store variable + if (this.autoReturnToCenter) { + this.movedX = this.centerX; + this.movedY = this.centerY; + } + // Delete canvas + this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); + // Redraw object + this.drawExternal(); + this.drawInternal(); + // Set attribute of callback + this.stickStatus.xPosition = this.movedX; + this.stickStatus.yPosition = this.movedY; + this.stickStatus.x = + 100 * ((this.movedX - this.centerX) / this.maxMoveStick); + this.stickStatus.y = + 100 * ((this.movedY - this.centerY) / this.maxMoveStick) * -1; + this.stickStatus.cardinalDirection = this.getCardinalDirection(); + this.callback(this.stickStatus); + } + getCardinalDirection() { + let result = ""; + const orizontal = this.movedX - this.centerX; + const vertical = this.movedY - this.centerY; + if (vertical >= this.directionVerticalLimitNeg && + vertical <= this.directionVerticalLimitPos) { + result = "C"; + } + if (vertical < this.directionVerticalLimitNeg) { + result = "N"; + } + if (vertical > this.directionVerticalLimitPos) { + result = "S"; + } + if (orizontal < this.directionHorizontalLimitNeg) { + if (result === "C") { + result = "W"; + } + else { + result += "W"; + } + } + if (orizontal > this.directionHorizontalLimitPos) { + if (result === "C") { + result = "E"; + } + else { + result += "E"; + } + } + return result; + } + /** + * @desc The width of canvas + * @return Number of pixel width + */ + getWidth() { + return this.canvas.width; + } + /** + * @desc The height of canvas + * @return Number of pixel height + */ + getHeigh() { + return this.canvas.height; + } + /** + * @desc The X position of the cursor relative to the canvas that contains it and to its dimensions + * @return Number that indicate relative position + */ + getPosX() { + return this.movedX; + } + /** + * @desc The Y position of the cursor relative to the canvas that contains it and to its dimensions + * @return Number that indicate relative position + */ + getPosY() { + return this.movedY; + } + /** + * @desc Normalizzed value of X move of stick + * @return Integer from -100 to +100 + */ + getX() { + return 100 * ((this.movedX - this.centerX) / this.maxMoveStick); + } + /** + * @desc Normalizzed value of Y move of stick + * @return Integer from -100 to +100 + */ + getY() { + return 100 * ((this.movedY - this.centerY) / this.maxMoveStick) * -1; + } + /** + * @desc Get the direction of the cursor as a string that indicates the cardinal points where this is oriented + * @return String of cardinal point N, NE, E, SE, S, SW, W, NW and C when it is placed in the center + */ + getDir() { + return this.getCardinalDirection(); + } +} +export default JoyStick; diff --git a/joy.js b/dist/origin/joy.js similarity index 100% rename from joy.js rename to dist/origin/joy.js diff --git a/joy.min.js b/dist/origin/joy.min.js similarity index 100% rename from joy.min.js rename to dist/origin/joy.min.js diff --git a/joy.html b/joy.html index 5f24cc8..667eeba 100644 --- a/joy.html +++ b/joy.html @@ -11,7 +11,7 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. @@ -73,7 +73,6 @@ border: 1px solid #0000FF; } - @@ -111,26 +110,27 @@

JoyStick

Direzione:
X :
Y : - + - diff --git a/package.json b/package.json index 876d29c..f9adfa6 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "html5-joystick", - "version": "2.0.0", + "version": "3.0.0", "description": "A simple JoyStick that use HTML5 Canvas and Vanilla JavaScript, for touch and mouse interfaces", - "main": "joy.js", + "main": "dist/index.js", + "types": "dist/index.d.ts", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "build": "tsc" }, "repository": { "type": "git", diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d8cb142 --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +export * from './joy'; diff --git a/src/joy.ts b/src/joy.ts new file mode 100644 index 0000000..d73ba90 --- /dev/null +++ b/src/joy.ts @@ -0,0 +1,529 @@ +/* + * Name : joy.js + * @author : Roberto D'Amico (Bobboteck) + * Last modified : 09.22.2023 + * Revision : 1.1.6 + * + * Modification History: + * Date Version Modified By Description + * 2023-09-22 3.0.0 cybaj Porting to TypeScript + * 2021-12-21 2.0.0 Roberto D'Amico New version of the project that integrates the callback functions, while + * maintaining compatibility with previous versions. Fixed Issue #27 too, + * thanks to @artisticfox8 for the suggestion. + * 2020-06-09 1.1.6 Roberto D'Amico Fixed Issue #10 and #11 + * 2020-04-20 1.1.5 Roberto D'Amico Correct: Two sticks in a row, thanks to @liamw9534 for the suggestion + * 2020-04-03 Roberto D'Amico Correct: InternalRadius when change the size of canvas, thanks to + * @vanslipon for the suggestion + * 2020-01-07 1.1.4 Roberto D'Amico Close #6 by implementing a new parameter to set the functionality of + * auto-return to 0 position + * 2019-11-18 1.1.3 Roberto D'Amico Close #5 correct indication of East direction + * 2019-11-12 1.1.2 Roberto D'Amico Removed Fix #4 incorrectly introduced and restored operation with touch + * devices + * 2019-11-12 1.1.1 Roberto D'Amico Fixed Issue #4 - Now JoyStick work in any position in the page, not only + * at 0,0 + * + * The MIT License (MIT) + * + * This file is part of the JoyStick Project (https://github.com/bobboteck/JoyStick). + * Copyright (c) 2015 Roberto D'Amico (Bobboteck). + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +export interface StickStatus { + xPosition: number; + yPosition: number; + x: number; + y: number; + cardinalDirection: CardinalDirection; +} +export type CardinalDirection = + | "C" + | "N" + | "NE" + | "E" + | "SE" + | "S" + | "SW" + | "W" + | "NW"; + +const defaultStickStatus: StickStatus = { + xPosition: 0, + yPosition: 0, + x: 0, + y: 0, + cardinalDirection: "C", +}; + +interface JoyStickMembers { + title: string; + width: number; + height: number; + internalFillColor: string; + internalLineWidth: number; + internalStrokeColor: string; + externalLineWidth: number; + externalStrokeColor: string; + autoReturnToCenter: boolean; + context: CanvasRenderingContext2D; + canvas: HTMLCanvasElement; + circumference: number; + internalRadius: number; + maxMoveStick: number; + xCenter: number; + yCenter: number; + movedX: number; + movedY: number; + pressed: boolean; + callback: JoyStickCallback; + stickStatus: StickStatus; +} + +export interface JoyStickParameters extends Partial {} + +const JoyStickDefaultParameters: JoyStickParameters = { + title: "joystick", + width: 0, + height: 0, + internalFillColor: "#00AA00", + internalLineWidth: 2, + internalStrokeColor: "#003300", + externalLineWidth: 2, + externalStrokeColor: "#008000", + autoReturnToCenter: true, +}; + +export type JoyStickCallback = (stickStatus: StickStatus) => void; + +export class JoyStick { + private title: string; + private width: number; + private height: number; + private internalFillColor: string; + private internalLineWidth: number; + private internalStrokeColor: string; + private externalLineWidth: number; + private externalStrokeColor: string; + private autoReturnToCenter: boolean; + private callback: JoyStickCallback; + private objContainer: HTMLElement | null; + private context: CanvasRenderingContext2D; + private canvas: HTMLCanvasElement; + private circumference: number; + private internalRadius: number; + private externalRadius: number; + private maxMoveStick: number; + private centerX: number; + private centerY: number; + private directionVerticalLimitPos: number; + private directionVerticalLimitNeg: number; + private directionHorizontalLimitPos: number; + private directionHorizontalLimitNeg: number; + private pressed: boolean; + private movedX: number; + private movedY: number; + private stickStatus: StickStatus; + + constructor( + containerId: string, + parameters: JoyStickParameters = {}, + callback: JoyStickCallback = () => {} + ) { + this.callback = callback; + + // Merge the user parameters with the default ones + // and check if the mandatory parameters are defined + // otherwise throw an error. + const mergedParameters = { + ...JoyStickDefaultParameters, + ...parameters, + } as JoyStickMembers; + this.title = mergedParameters.title; + this.width = mergedParameters.width!; + this.height = mergedParameters.height!; + this.internalFillColor = mergedParameters.internalFillColor!; + this.internalLineWidth = mergedParameters.internalLineWidth!; + this.internalStrokeColor = mergedParameters.internalStrokeColor!; + this.externalLineWidth = mergedParameters.externalLineWidth!; + this.externalStrokeColor = mergedParameters.externalStrokeColor!; + this.autoReturnToCenter = mergedParameters.autoReturnToCenter!; + + this.objContainer = document.getElementById(containerId); + + // Fixing Unable to preventDefault inside passive event listener due to target being treated as passive in Chrome [Thanks to https://github.com/artisticfox8 for this suggestion] + if (!this.objContainer) { + throw new Error( + `The container with id '${containerId}' was not found in the DOM.` + ); + } + this.objContainer.style.touchAction = "none"; + + this.canvas = document.createElement("canvas"); + this.canvas.id = this.title; + if (this.width === 0) { + this.width = this.objContainer.clientWidth; + } + if (this.height === 0) { + this.height = this.objContainer.clientHeight; + } + this.canvas.width = this.width; + this.canvas.height = this.height; + this.objContainer.appendChild(this.canvas); + const context = this.canvas.getContext("2d"); + if (!context) { + throw new Error("Unable to get the 2D context from the canvas."); + } + this.context = context; + + // configuration of the Joystick + this.circumference = 2 * Math.PI; + this.internalRadius = + (this.canvas.width - (this.canvas.width / 2 + 10)) / 2; + this.maxMoveStick = this.internalRadius + 5; + this.externalRadius = this.internalRadius + 30; + this.centerX = this.canvas.width / 2; + this.centerY = this.canvas.height / 2; + this.directionHorizontalLimitPos = this.canvas.width / 10; + this.directionHorizontalLimitNeg = this.directionHorizontalLimitPos * -1; + this.directionVerticalLimitPos = this.canvas.height / 10; + this.directionVerticalLimitNeg = this.directionVerticalLimitPos * -1; + + this.stickStatus = defaultStickStatus; + this.pressed = false; + // Used to save current position of stick + this.movedX = this.centerX; + this.movedY = this.centerY; + + // Check if the device support the touch or not + if ("ontouchstart" in document.documentElement) { + this.canvas.addEventListener( + "touchstart", + this.onTouchStart.bind(this), + false + ); + document.addEventListener( + "touchmove", + this.onTouchMove.bind(this), + false + ); + document.addEventListener("touchend", this.onTouchEnd.bind(this), false); + } else { + this.canvas.addEventListener( + "mousedown", + this.onMouseDown.bind(this), + false + ); + document.addEventListener( + "mousemove", + this.onMouseMove.bind(this), + false + ); + document.addEventListener("mouseup", this.onMouseUp.bind(this), false); + } + + // Draw the object + this.drawExternal(); + this.drawInternal(); + } + + /** + * @desc Draw the external circle used as reference position + */ + private drawExternal() { + this.context.beginPath(); + this.context.arc( + this.centerX, + this.centerY, + this.externalRadius, + 0, + this.circumference, + false + ); + this.context.lineWidth = this.externalLineWidth; + this.context.strokeStyle = this.externalStrokeColor; + this.context.stroke(); + } + + /** + * @desc Draw the internal stick in the current position the user have moved it + */ + private drawInternal() { + this.context.beginPath(); + if (this.movedX < this.internalRadius) { + this.movedX = this.maxMoveStick; + } + if (this.movedX + this.internalRadius > this.canvas.width) { + this.movedX = this.canvas.width - this.maxMoveStick; + } + if (this.movedY < this.internalRadius) { + this.movedY = this.maxMoveStick; + } + if (this.movedY + this.internalRadius > this.canvas.height) { + this.movedY = this.canvas.height - this.maxMoveStick; + } + this.context.arc( + this.movedX, + this.movedY, + this.internalRadius, + 0, + this.circumference, + false + ); + // create radial gradient + var grd = this.context.createRadialGradient( + this.centerX, + this.centerY, + 5, + this.centerX, + this.centerY, + 200 + ); + // Light color + grd.addColorStop(0, this.internalFillColor); + // Dark color + grd.addColorStop(1, this.internalStrokeColor); + this.context.fillStyle = grd; + this.context.fill(); + this.context.lineWidth = this.internalLineWidth; + this.context.strokeStyle = this.internalStrokeColor; + this.context.stroke(); + } + + /** + * @desc Events for manage touch + */ + private onTouchStart(event: TouchEvent) { + this.pressed = true; + } + + private onTouchMove(event: TouchEvent) { + if (this.pressed && event.targetTouches[0].target === this.canvas) { + this.movedX = event.targetTouches[0].pageX; + this.movedY = event.targetTouches[0].pageY; + // Manage offset + if (this.canvas.offsetParent?.tagName.toUpperCase() === "BODY") { + this.movedX -= this.canvas.offsetLeft; + this.movedY -= this.canvas.offsetTop; + } else { + // TODO delete offsetParent dependency + // @ts-ignore + this.movedX -= this.canvas.offsetParent?.offsetLeft; + // @ts-ignore + this.movedY -= this.canvas.offsetParent?.offsetTop; + } + // Delete canvas + this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); + // Redraw object + this.drawExternal(); + this.drawInternal(); + + // Set attribute of callback + this.stickStatus.xPosition = this.movedX; + this.stickStatus.yPosition = this.movedY; + this.stickStatus.x = + 100 * ((this.movedX - this.centerX) / this.maxMoveStick); + this.stickStatus.y = + 100 * ((this.movedY - this.centerY) / this.maxMoveStick) * -1; + this.stickStatus.cardinalDirection = this.getCardinalDirection(); + this.callback(this.stickStatus); + } + } + + private onTouchEnd(event: TouchEvent): void { + this.pressed = false; + // If required reset position store variable + if (this.autoReturnToCenter) { + this.movedX = this.centerX; + this.movedY = this.centerY; + } + // Delete canvas + this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); + // Redraw object + this.drawExternal(); + this.drawInternal(); + + // Set attribute of callback + this.stickStatus.xPosition = this.movedX; + this.stickStatus.yPosition = this.movedY; + this.stickStatus.x = + 100 * ((this.movedX - this.centerX) / this.maxMoveStick); + this.stickStatus.y = + 100 * ((this.movedY - this.centerY) / this.maxMoveStick) * -1; + this.stickStatus.cardinalDirection = this.getCardinalDirection(); + this.callback(this.stickStatus); + } + + /** + * @desc Events for manage mouse + */ + private onMouseDown(event: MouseEvent) { + this.pressed = true; + } + + /* To simplify this code there was a new experimental feature here: https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/offsetX , but it present only in Mouse case not metod presents in Touch case :-( */ + private onMouseMove(event: MouseEvent) { + if (this.pressed) { + this.movedX = event.pageX; + this.movedY = event.pageY; + // Manage offset + if (this.canvas.offsetParent?.tagName.toUpperCase() === "BODY") { + this.movedX -= this.canvas.offsetLeft; + this.movedY -= this.canvas.offsetTop; + } else { + // @ts-ignore + this.movedX -= this.canvas.offsetParent?.offsetLeft; + // @ts-ignore + this.movedY -= this.canvas.offsetParent?.offsetTop; + } + // Delete canvas + this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); + // Redraw object + this.drawExternal(); + this.drawInternal(); + + // Set attribute of callback + this.stickStatus.xPosition = this.movedX; + this.stickStatus.yPosition = this.movedY; + this.stickStatus.x = + 100 * ((this.movedX - this.centerX) / this.maxMoveStick); + this.stickStatus.y = + 100 * ((this.movedY - this.centerY) / this.maxMoveStick) * -1; + this.stickStatus.cardinalDirection = this.getCardinalDirection(); + this.callback(this.stickStatus); + } + } + + private onMouseUp(event: MouseEvent): void { + this.pressed = false; + // If required reset position store variable + if (this.autoReturnToCenter) { + this.movedX = this.centerX; + this.movedY = this.centerY; + } + // Delete canvas + this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); + // Redraw object + this.drawExternal(); + this.drawInternal(); + + // Set attribute of callback + this.stickStatus.xPosition = this.movedX; + this.stickStatus.yPosition = this.movedY; + this.stickStatus.x = + 100 * ((this.movedX - this.centerX) / this.maxMoveStick); + this.stickStatus.y = + 100 * ((this.movedY - this.centerY) / this.maxMoveStick) * -1; + this.stickStatus.cardinalDirection = this.getCardinalDirection(); + this.callback(this.stickStatus); + } + + getCardinalDirection() { + let result = ""; + const orizontal = this.movedX - this.centerX; + const vertical = this.movedY - this.centerY; + + if ( + vertical >= this.directionVerticalLimitNeg && + vertical <= this.directionVerticalLimitPos + ) { + result = "C"; + } + if (vertical < this.directionVerticalLimitNeg) { + result = "N"; + } + if (vertical > this.directionVerticalLimitPos) { + result = "S"; + } + + if (orizontal < this.directionHorizontalLimitNeg) { + if (result === "C") { + result = "W"; + } else { + result += "W"; + } + } + if (orizontal > this.directionHorizontalLimitPos) { + if (result === "C") { + result = "E"; + } else { + result += "E"; + } + } + + return result as CardinalDirection; + } + /** + * @desc The width of canvas + * @return Number of pixel width + */ + public getWidth() { + return this.canvas.width; + } + + /** + * @desc The height of canvas + * @return Number of pixel height + */ + public getHeigh() { + return this.canvas.height; + } + + /** + * @desc The X position of the cursor relative to the canvas that contains it and to its dimensions + * @return Number that indicate relative position + */ + public getPosX() { + return this.movedX; + } + + /** + * @desc The Y position of the cursor relative to the canvas that contains it and to its dimensions + * @return Number that indicate relative position + */ + public getPosY() { + return this.movedY; + } + + /** + * @desc Normalizzed value of X move of stick + * @return Integer from -100 to +100 + */ + public getX() { + return 100 * ((this.movedX - this.centerX) / this.maxMoveStick); + } + + /** + * @desc Normalizzed value of Y move of stick + * @return Integer from -100 to +100 + */ + public getY() { + return 100 * ((this.movedY - this.centerY) / this.maxMoveStick) * -1; + } + + /** + * @desc Get the direction of the cursor as a string that indicates the cardinal points where this is oriented + * @return String of cardinal point N, NE, E, SE, S, SW, W, NW and C when it is placed in the center + */ + public getDir() { + return this.getCardinalDirection(); + } +} + +export default JoyStick; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..16b2de9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,109 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "esnext", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +}