From 8b49b44ebf462abd2fce0fb17e6dda47fe84466e Mon Sep 17 00:00:00 2001 From: schmelczerandras Date: Fri, 23 Oct 2020 18:06:56 +0200 Subject: [PATCH] Add touch joystick movement --- frontend/package.json | 2 +- frontend/src/main.scss | 29 ++++- .../generators/touch-joystick-listener.ts | 108 ++++++++++++++++++ .../commands/generators/touch-listener.ts | 80 ------------- frontend/src/scripts/game.ts | 16 +-- frontend/webpack.config.js | 19 ++- shared/src/helper/last.ts | 2 +- 7 files changed, 161 insertions(+), 95 deletions(-) create mode 100644 frontend/src/scripts/commands/generators/touch-joystick-listener.ts delete mode 100644 frontend/src/scripts/commands/generators/touch-listener.ts diff --git a/frontend/package.json b/frontend/package.json index 595ff56..6951198 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,6 +30,7 @@ "eslint-plugin-unused-imports": "^0.1.3", "firebase": "^7.22.0", "gl-matrix": "^3.3.0", + "html-webpack-inline-source-plugin": "^1.0.0-beta.2", "html-webpack-inline-svg-plugin": "^2.3.0", "html-webpack-plugin": "^4.5.0", "image-config-webpack-plugin": "^2.0.0", @@ -41,7 +42,6 @@ "shared": "file:../shared", "socket.io-client": "^2.3.1", "source-map-loader": "^1.1.1", - "style-ext-html-webpack-plugin": "^4.1.2", "svg-url-loader": "^6.0.0", "terser-webpack-plugin": "^4.2.2", "ts-config-webpack-plugin": "^2.0.0", diff --git a/frontend/src/main.scss b/frontend/src/main.scss index ca5a121..1dcf3ef 100644 --- a/frontend/src/main.scss +++ b/frontend/src/main.scss @@ -83,18 +83,25 @@ body { width: 100%; position: absolute; top: 0; + left: 0; pointer-events: none; user-select: none; overflow: hidden; + & > * { + display: inline-block; + position: absolute; + top: 0; + left: 0; + } + h2 { margin: $large-padding 0; } .player-tag { - position: absolute; transform: translateX(-50%) translateY(-50%) rotate(-15deg); transition: left 200ms, top 200ms; border-radius: 1000px; @@ -139,7 +146,6 @@ body { } .ownership { - position: absolute; font-size: 0; transform: translateX(-50%) translateY(-50%); @@ -165,6 +171,25 @@ body { } } + .joystick { + @include square($large-icon * 1.3); + background-color: white; + box-shadow: inset 0 0 8px 3px rgba(0, 0, 0, 0.33); + opacity: 0.35; + border-radius: 1000px; + + div { + position: absolute; + top: 50%; + left: 50%; + background-color: #444; + box-shadow: 0 0 8px 3px rgba(0, 0, 0, 0.33); + + @include square($small-icon); + border-radius: 1000px; + } + } + .planet-progress { position: absolute; top: $small-padding; diff --git a/frontend/src/scripts/commands/generators/touch-joystick-listener.ts b/frontend/src/scripts/commands/generators/touch-joystick-listener.ts new file mode 100644 index 0000000..0887190 --- /dev/null +++ b/frontend/src/scripts/commands/generators/touch-joystick-listener.ts @@ -0,0 +1,108 @@ +import { vec2 } from 'gl-matrix'; +import { + CommandGenerator, + SecondaryActionCommand, + TernaryActionCommand, + MoveActionCommand, + last, +} from 'shared'; +import { Game } from '../../game'; +import { OptionsHandler } from '../../options-handler'; + +export class TouchJoystickListener extends CommandGenerator { + private joystick: HTMLElement; + private joystickButton: HTMLElement; + + private isJoystickActive = false; + private touchStartPosition!: vec2; + + constructor( + target: HTMLElement, + private overlay: HTMLElement, + private readonly game: Game, + ) { + super(); + + this.joystick = document.createElement('div'); + this.joystick.className = 'joystick'; + this.joystickButton = document.createElement('div'); + this.joystick.appendChild(this.joystickButton); + + target.addEventListener('touchstart', (event: TouchEvent) => { + event.preventDefault(); + if (this.isJoystickActive) { + const center = vec2.fromValues( + last(event.touches)!.clientX, + last(event.touches)!.clientY, + ); + this.sendCommandToSubcribers( + new SecondaryActionCommand(this.game.displayToWorldCoordinates(center)), + ); + } + }); + + target.addEventListener('touchmove', (event: TouchEvent) => { + event.preventDefault(); + + if (!this.isJoystickActive) { + this.isJoystickActive = true; + this.overlay.appendChild(this.joystick); + this.touchStartPosition = vec2.fromValues( + event.touches[0].clientX, + event.touches[0].clientY, + ); + this.joystickButton.style.transform = `translateX(-50%) translateY(-50%)`; + this.joystick.style.transform = `translateX(${this.touchStartPosition.x}px) translateY(${this.touchStartPosition.y}px) translateX(-50%) translateY(-50%)`; + } + + const touchPosition = vec2.fromValues( + event.touches[0].clientX, + event.touches[0].clientY, + ); + + const movement = vec2.subtract( + vec2.create(), + touchPosition, + this.touchStartPosition, + ); + + vec2.scale(movement, movement, 1 / 3); + const length = vec2.length(movement); + const maxLength = 20; + vec2.scale(movement, movement, Math.min(1, maxLength / length)); + this.joystickButton.style.transform = `translateX(${movement.x}px) translateY(${movement.y}px) translateX(-50%) translateY(-50%)`; + + vec2.set(movement, movement.x, -movement.y); + if (vec2.squaredLength(movement) > 0) { + vec2.normalize(movement, movement); + + this.sendCommandToSubcribers( + new MoveActionCommand(movement, OptionsHandler.options.relativeMovementEnabled), + ); + } + }); + + target.addEventListener('touchend', (event: TouchEvent) => { + event.preventDefault(); + + if (!this.isJoystickActive) { + const center = vec2.fromValues( + event.changedTouches[0].clientX, + event.changedTouches[0].clientY, + ); + this.sendCommandToSubcribers( + new SecondaryActionCommand(this.game.displayToWorldCoordinates(center)), + ); + } else if (event.touches.length === 0) { + this.isJoystickActive = false; + this.joystick.parentElement?.removeChild(this.joystick); + this.sendCommandToSubcribers( + new MoveActionCommand( + vec2.create(), + OptionsHandler.options.relativeMovementEnabled, + ), + ); + } + }); + } +} diff --git a/frontend/src/scripts/commands/generators/touch-listener.ts b/frontend/src/scripts/commands/generators/touch-listener.ts deleted file mode 100644 index 45bc024..0000000 --- a/frontend/src/scripts/commands/generators/touch-listener.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { - CommandGenerator, - PrimaryActionCommand, - SecondaryActionCommand, - TernaryActionCommand, - MoveActionCommand, -} from 'shared'; -import { Game } from '../../game'; -import { OptionsHandler } from '../../options-handler'; - -export class TouchListener extends CommandGenerator { - private previousPosition = vec2.create(); - - constructor(target: HTMLElement, private readonly game: Game) { - super(); - - target.addEventListener('touchstart', (event: TouchEvent) => { - event.preventDefault(); - - const touchCount = event.touches.length; - const position = this.positionFromEvent(event); - this.previousPosition = position; - - if (touchCount == 1) { - this.sendCommandToSubcribers(new PrimaryActionCommand(position)); - } else if (touchCount == 2) { - this.sendCommandToSubcribers(new SecondaryActionCommand(position)); - } else { - this.sendCommandToSubcribers(new TernaryActionCommand(position)); - } - }); - - target.addEventListener('touchmove', (event: TouchEvent) => { - event.preventDefault(); - - const position = this.positionFromEvent(event, true); - const movement = vec2.subtract(vec2.create(), position, this.previousPosition); - - if (vec2.squaredLength(movement) > 0) { - vec2.normalize(movement, movement); - this.sendCommandToSubcribers( - new MoveActionCommand(movement, OptionsHandler.options.relativeMovementEnabled), - ); - } - - this.previousPosition = position; - }); - - target.addEventListener('touchend', (event: TouchEvent) => { - event.preventDefault(); - this.sendCommandToSubcribers( - new MoveActionCommand( - vec2.create(), - OptionsHandler.options.relativeMovementEnabled, - ), - ); - }); - } - - private positionFromEvent(event: TouchEvent, invert = false): vec2 { - const touches = Array.prototype.slice.call(event.touches); - const center = touches.reduce( - (center: vec2, touch: Touch) => - vec2.add( - center, - center, - vec2.fromValues( - touch.clientX * (invert ? -1 : 1), - touch.clientY * (invert ? -1 : 1), - ), - ), - vec2.create(), - ); - - return this.game.displayToWorldCoordinates( - vec2.scale(center, center, 1 / event.touches.length), - ); - } -} diff --git a/frontend/src/scripts/game.ts b/frontend/src/scripts/game.ts index b5f52bc..2e2da55 100644 --- a/frontend/src/scripts/game.ts +++ b/frontend/src/scripts/game.ts @@ -24,7 +24,7 @@ import { import io from 'socket.io-client'; import { KeyboardListener } from './commands/generators/keyboard-listener'; import { MouseListener } from './commands/generators/mouse-listener'; -import { TouchListener } from './commands/generators/touch-listener'; +import { TouchJoystickListener } from './commands/generators/touch-joystick-listener'; import { CommandReceiverSocket } from './commands/receivers/command-receiver-socket'; import { PlayerDecision } from './join-form-handler'; import { GameObjectContainer } from './objects/game-object-container'; @@ -129,15 +129,15 @@ export class Game { const delta = vec2.fromValues(deltaX, deltaY); const center = vec2.fromValues(width / 2, height / 2); const p = vec2.add(center, center, delta); - const arrowPadding = 16; + const arrowPadding = 24; vec2.set( p, - clamp(p.x, arrowPadding, width - 3 * arrowPadding), - clamp(height - p.y, arrowPadding, height - 3 * arrowPadding), + clamp(p.x, arrowPadding, width - arrowPadding), + clamp(height - p.y, arrowPadding, height - arrowPadding), ); - e.style.transform = `translateX(${p.x}px) translateY(${p.y}px) rotate(${ - -angle + Math.PI / 2 - }rad) translateX(-50%) translateY(-50%)`; + e.style.transform = `translateX(${p.x}px) translateY(${ + p.y + }px) translateX(-50%) translateY(-50%) rotate(${-angle + Math.PI / 2}rad) `; }); } else this.gameObjects.sendCommand(command); }); @@ -154,7 +154,7 @@ export class Game { [ new KeyboardListener(document.body), new MouseListener(this.canvas, this), - new TouchListener(this.canvas, this), + new TouchJoystickListener(this.canvas, this.overlay, this), ], [this.gameObjects, new CommandReceiverSocket(this.socket)], ); diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index 2c61699..4713038 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -4,8 +4,8 @@ const ScssConfigWebpackPlugin = require('scss-config-webpack-plugin'); const TsConfigWebpackPlugin = require('ts-config-webpack-plugin'); const HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin'); const TerserJSPlugin = require('terser-webpack-plugin'); +const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin'); //const FaviconsWebpackPlugin = require('favicons-webpack-plugin'); -//const StyleExtHtmlWebpackPlugin = require('style-ext-html-webpack-plugin'); module.exports = { devServer: { @@ -18,10 +18,14 @@ module.exports = { plugins: [ // Cleans the dist folder before the build starts new CleanWebpackPlugin(), + new ScssConfigWebpackPlugin(), + // Generate a base html file and injects all generated css and js files new HtmlWebpackPlugin({ template: './src/index.html', + inlineSource: '.(css)$', }), + new HtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin), new HtmlWebpackInlineSVGPlugin({ inlineAll: true, svgoConfig: [ @@ -30,11 +34,10 @@ module.exports = { }, ], }), - //new StyleExtHtmlWebpackPlugin('main.css'), //new FaviconsWebpackPlugin('static/logo.svg'), // SCSS Configuration for .css .module.css and .scss .module.scss files // see https://github.com/namics/webpack-config-plugins/tree/master/packages/scss-config-webpack-plugin/config - new ScssConfigWebpackPlugin(), + // Multi threading typescript loader configuration with caching for .ts and .tsx files // see https://github.com/namics/webpack-config-plugins/tree/master/packages/ts-config-webpack-plugin/config new TsConfigWebpackPlugin(), @@ -57,6 +60,16 @@ module.exports = { enforce: 'pre', use: ['source-map-loader'], }, + { + test: /\.css$/, + use: [ + { + loader: 'style-loader', + options: { injectType: 'singletonStyleTag' }, + }, + 'css-loader', + ], + }, { test: /\.svg/, use: { diff --git a/shared/src/helper/last.ts b/shared/src/helper/last.ts index 5c319b0..39a5c1f 100644 --- a/shared/src/helper/last.ts +++ b/shared/src/helper/last.ts @@ -1,3 +1,3 @@ -export function last(a: Array): T | null { +export function last(a: ArrayLike): T | null { return a.length > 0 ? a[a.length - 1] : null; }