From 66a679bea32e8f326910e4e1de1d08d0712b1ee4 Mon Sep 17 00:00:00 2001 From: zr3 Date: Fri, 16 Feb 2024 19:44:28 -0800 Subject: [PATCH] feat: add virtual joystick and improve mobile ux --- delightful-thyme/index.html | 9 +- delightful-thyme/main.js | 18 +- delightful-thyme/style.css | 114 ++++++- delightful-thyme/virtualjoystick.js | 477 ++++++++++++++++++++++++++++ 4 files changed, 595 insertions(+), 23 deletions(-) create mode 100644 delightful-thyme/virtualjoystick.js diff --git a/delightful-thyme/index.html b/delightful-thyme/index.html index 07a1a72..239664b 100644 --- a/delightful-thyme/index.html +++ b/delightful-thyme/index.html @@ -51,14 +51,15 @@

[ENTER] : see what is on the screen (scan all)

[MOUSE HOVER] : see what is on the screen (under mouse)

+
- - - - + + + + diff --git a/delightful-thyme/main.js b/delightful-thyme/main.js index 558910a..07c59de 100644 --- a/delightful-thyme/main.js +++ b/delightful-thyme/main.js @@ -1,4 +1,5 @@ import "./style.css"; +import { VirtualJoystick } from "./virtualjoystick"; globalThis.gameStats = { playerName: "???", @@ -28,13 +29,22 @@ function triggerKey(id, key) { }); } -triggerKey('gc-left', 'KeyH'); -triggerKey('gc-right', 'KeyL'); -triggerKey('gc-up', 'KeyK'); -triggerKey('gc-down', 'KeyJ'); +// triggerKey('gc-left', 'KeyH'); +// triggerKey('gc-right', 'KeyL'); +// triggerKey('gc-up', 'KeyK'); +// triggerKey('gc-down', 'KeyJ'); triggerKey('gc-interact', 'Space'); triggerKey('gc-back', 'Backspace'); triggerKey('gc-inventory', 'KeyI'); triggerKey('gc-equipment', 'KeyE'); triggerKey('gc-drop', 'KeyD'); triggerKey('gc-scan', 'Enter'); + +new VirtualJoystick({ + mouseSupport : true, + limitStickTravel: true, + stickRadius : 50, + container: document.getElementById('joystick-container'), + strokeStyle: '#aaaaaa', +}); + diff --git a/delightful-thyme/style.css b/delightful-thyme/style.css index 44349d3..5956208 100644 --- a/delightful-thyme/style.css +++ b/delightful-thyme/style.css @@ -1,7 +1,8 @@ :root { --color-dark: #041725; --current-level: 1; - --background-level-scale: 100vw + --background-level-scale-vw: 110vw; + --background-level-scale-vh: 110vh; } html { background-color: var(--color-dark); @@ -16,12 +17,10 @@ body { } #game-layout { - transition: all 3s ease-in-out; + transition: backdrop-filter 3s ease-in-out; backdrop-filter: brightness(calc(1 - var(--current-level) * 0.1)); background-color: var(--color-dark); background-image: url('img/bg.avif'); - max-width: 1800px; - margin: auto; width: 100vw; height: 100vh; display: grid; @@ -147,7 +146,7 @@ body { } /* desktop-only layout */ -@media (min-width: 601px) { +@media (min-width: 1001px) { html { font-size: 12px; } @@ -201,8 +200,85 @@ body { cursor: pointer; } } +/* mobile-wide layout */ +@media (min-width: 601px) and (max-width: 1000px) { + html { + font-size: 12px; + } + .type-animation { + width: 25ch; + animation: typing 1.1s steps(25); + } + #top-text { + position: relative; + font-size: 1rem; + } + #game-layout { + display: grid; + grid-template-columns: 1fr 26rem; + grid-template-rows: repeat(3, 1fr); + grid-column-gap: 0px; + grid-row-gap: 0px; + background-position: center; + background-size: var(--background-level-scale-vw); + } + #input-window { + grid-area: 1 / 1 / 4 / 2; + } + #game-window { + grid-area: 1 / 1 / 4 / 2; + display: flex; + flex-direction: column; + justify-content: center; + } + #top-text { + font-size: 0.8rem; + margin-top: 2rem; + grid-area: 1 / 2 / 3 / 3; + overflow-y: scroll; + } + #bottom-text { + display: none; + } + #joystick-container { + grid-area: 1 / 1 / 4 / 2; + } + #game-controls { + padding: 1rem; + grid-area: 3 / 2 / 4 / 3; + } + #gc-button-pad { + display: grid; + grid-template-columns: repeat(5, 1fr); + grid-template-rows: repeat(2, 1fr); + /* grid-template-rows: repeat(5, 1fr); */ + grid-column-gap: 0px; + grid-row-gap: 0px; + } + #gc-interact { grid-area: 1 / 1 / 2 / 3; } + #gc-back { grid-area: 1 / 4 / 2 / 6; } + #gc-inventory { grid-area: 2 / 1 / 3 / 2; } + #gc-equipment { grid-area: 2 / 2 / 3 / 3; } + #gc-drop { grid-area: 2 / 4 / 3 / 5; } + #gc-scan { grid-area: 2 / 5 / 3 / 6; } + #fullscreen-controls { + display: none; + } + + .gc-button { + background: transparent; + border: 1px dashed #aaa; + border-radius: 5px; + color: #aaa; + height: 40px; + font-family: "PC Senior", monospace; + margin: 2px; + box-shadow: 5px 5px #aaa; + font-size: 0.5rem; + } +} -/* mobile-only layout */ +/* mobile-tall layout */ @media (max-width: 600px) { html { font-size: calc(5px + 1.3vh); @@ -235,6 +311,9 @@ body { #fullscreen-controls { display: none; } + #joystick-container { + grid-area: 1 / 1 / 3 / 3; + } #game-controls { grid-area: 3 / 1 / 4 / 3; padding: 2rem; @@ -243,20 +322,25 @@ body { #gc-button-pad { display: grid; grid-template-columns: repeat(5, 1fr); - grid-template-rows: repeat(5, 1fr); + grid-template-rows: repeat(2, 1fr); + /* grid-template-rows: repeat(5, 1fr); */ grid-column-gap: 0px; grid-row-gap: 0px; } #gc-interact { grid-area: 1 / 1 / 2 / 3; } #gc-back { grid-area: 1 / 4 / 2 / 6; } - #gc-up { grid-area: 2 / 3 / 3 / 4; } - #gc-left { grid-area: 3 / 2 / 4 / 3; } - #gc-right { grid-area: 3 / 4 / 4 / 5; } - #gc-down { grid-area: 4 / 3 / 5 / 4; } - #gc-inventory { grid-area: 5 / 1 / 6 / 2; } - #gc-equipment { grid-area: 5 / 2 / 6 / 3; } - #gc-drop { grid-area: 5 / 4 / 6 / 5; } - #gc-scan { grid-area: 5 / 5 / 6 / 6; } + /* #gc-up { grid-area: 2 / 3 / 3 / 4; } */ + /* #gc-left { grid-area: 3 / 2 / 4 / 3; } */ + /* #gc-right { grid-area: 3 / 4 / 4 / 5; } */ + /* #gc-down { grid-area: 4 / 3 / 5 / 4; } */ + /* #gc-inventory { grid-area: 5 / 1 / 6 / 2; } */ + /* #gc-equipment { grid-area: 5 / 2 / 6 / 3; } */ + /* #gc-drop { grid-area: 5 / 4 / 6 / 5; } */ + /* #gc-scan { grid-area: 5 / 5 / 6 / 6; } */ + #gc-inventory { grid-area: 2 / 1 / 3 / 2; } + #gc-equipment { grid-area: 2 / 2 / 3 / 3; } + #gc-drop { grid-area: 2 / 4 / 3 / 5; } + #gc-scan { grid-area: 2 / 5 / 3 / 6; } .gc-button { background: transparent; diff --git a/delightful-thyme/virtualjoystick.js b/delightful-thyme/virtualjoystick.js new file mode 100644 index 0000000..0fdf8e0 --- /dev/null +++ b/delightful-thyme/virtualjoystick.js @@ -0,0 +1,477 @@ +// https://github.com/jeromeetienne/virtualjoystick.js/blob/master/virtualjoystick.js + +export var VirtualJoystick = function(opts) +{ + opts = opts || {}; + this._container = opts.container || document.body; + this._strokeStyle = opts.strokeStyle || 'cyan'; + this._stickEl = opts.stickElement || this._buildJoystickStick(); + this._baseEl = opts.baseElement || this._buildJoystickBase(); + this._mouseSupport = opts.mouseSupport !== undefined ? opts.mouseSupport : false; + this._stationaryBase = opts.stationaryBase || false; + this._baseX = this._stickX = opts.baseX || 0 + this._baseY = this._stickY = opts.baseY || 0 + this._limitStickTravel = opts.limitStickTravel || false + this._stickRadius = opts.stickRadius !== undefined ? opts.stickRadius : 100 + this._useCssTransform = opts.useCssTransform !== undefined ? opts.useCssTransform : false + + this._container.style.position = "relative" + + this._container.appendChild(this._baseEl) + this._baseEl.style.position = "absolute" + this._baseEl.style.display = "none" + this._container.appendChild(this._stickEl) + this._stickEl.style.position = "absolute" + this._stickEl.style.display = "none" + + this._pressed = false; + this._touchIdx = null; + + if(this._stationaryBase === true){ + this._baseEl.style.display = ""; + this._baseEl.style.left = (this._baseX - this._baseEl.width /2)+"px"; + this._baseEl.style.top = (this._baseY - this._baseEl.height/2)+"px"; + } + + this._transform = this._useCssTransform ? this._getTransformProperty() : false; + this._has3d = this._check3D(); + + var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + this._$onTouchStart = __bind(this._onTouchStart , this); + this._$onTouchEnd = __bind(this._onTouchEnd , this); + this._$onTouchMove = __bind(this._onTouchMove , this); + this._container.addEventListener( 'touchstart' , this._$onTouchStart , false ); + this._container.addEventListener( 'touchend' , this._$onTouchEnd , false ); + this._container.addEventListener( 'touchmove' , this._$onTouchMove , false ); + if( this._mouseSupport ){ + this._$onMouseDown = __bind(this._onMouseDown , this); + this._$onMouseUp = __bind(this._onMouseUp , this); + this._$onMouseMove = __bind(this._onMouseMove , this); + this._container.addEventListener( 'mousedown' , this._$onMouseDown , false ); + this._container.addEventListener( 'mouseup' , this._$onMouseUp , false ); + this._container.addEventListener( 'mousemove' , this._$onMouseMove , false ); + } +} + +VirtualJoystick.prototype.destroy = function() +{ + this._container.removeChild(this._baseEl); + this._container.removeChild(this._stickEl); + + this._container.removeEventListener( 'touchstart' , this._$onTouchStart , false ); + this._container.removeEventListener( 'touchend' , this._$onTouchEnd , false ); + this._container.removeEventListener( 'touchmove' , this._$onTouchMove , false ); + if( this._mouseSupport ){ + this._container.removeEventListener( 'mouseup' , this._$onMouseUp , false ); + this._container.removeEventListener( 'mousedown' , this._$onMouseDown , false ); + this._container.removeEventListener( 'mousemove' , this._$onMouseMove , false ); + } +} + +/** + * @returns {Boolean} true if touchscreen is currently available, false otherwise +*/ +VirtualJoystick.touchScreenAvailable = function() +{ + return 'createTouch' in document ? true : false; +} + +/** + * microevents.js - https://github.com/jeromeetienne/microevent.js +*/ +;(function(destObj){ + destObj.addEventListener = function(event, fct){ + if(this._events === undefined) this._events = {}; + this._events[event] = this._events[event] || []; + this._events[event].push(fct); + return fct; + }; + destObj.removeEventListener = function(event, fct){ + if(this._events === undefined) this._events = {}; + if( event in this._events === false ) return; + this._events[event].splice(this._events[event].indexOf(fct), 1); + }; + destObj.dispatchEvent = function(event /* , args... */){ + if(this._events === undefined) this._events = {}; + if( this._events[event] === undefined ) return; + var tmpArray = this._events[event].slice(); + for(var i = 0; i < tmpArray.length; i++){ + var result = tmpArray[i].apply(this, Array.prototype.slice.call(arguments, 1)) + if( result !== undefined ) return result; + } + return undefined + }; +})(VirtualJoystick.prototype); + +////////////////////////////////////////////////////////////////////////////////// +// // +////////////////////////////////////////////////////////////////////////////////// + +VirtualJoystick.prototype.deltaX = function(){ return this._stickX - this._baseX; } +VirtualJoystick.prototype.deltaY = function(){ return this._stickY - this._baseY; } + +VirtualJoystick.prototype.up = function(){ + if( this._pressed === false ) return false; + var deltaX = this.deltaX(); + var deltaY = this.deltaY(); + if( deltaY >= 0 ) return false; + if( Math.abs(deltaX) > 2*Math.abs(deltaY) ) return false; + return true; +} +VirtualJoystick.prototype.down = function(){ + if( this._pressed === false ) return false; + var deltaX = this.deltaX(); + var deltaY = this.deltaY(); + if( deltaY <= 0 ) return false; + if( Math.abs(deltaX) > 2*Math.abs(deltaY) ) return false; + return true; +} +VirtualJoystick.prototype.right = function(){ + if( this._pressed === false ) return false; + var deltaX = this.deltaX(); + var deltaY = this.deltaY(); + if( deltaX <= 0 ) return false; + if( Math.abs(deltaY) > 2*Math.abs(deltaX) ) return false; + return true; +} +VirtualJoystick.prototype.left = function(){ + if( this._pressed === false ) return false; + var deltaX = this.deltaX(); + var deltaY = this.deltaY(); + if( deltaX >= 0 ) return false; + if( Math.abs(deltaY) > 2*Math.abs(deltaX) ) return false; + return true; +} + +////////////////////////////////////////////////////////////////////////////////// +// // +////////////////////////////////////////////////////////////////////////////////// + +const tan_Pi_div_8 = Math.sqrt(2.0) - 1.0; + +function getCardinalDirection(dx, dy) { + const ddx = Math.abs(dx); + const ddy = Math.abs(dy); + const d = Math.sqrt(dx * dx + dy * dy); + if (d < 35) { + return 'none'; + } + if (ddx > ddy) { + if (ddy / ddx <= tan_Pi_div_8) { + return dx > 0 ? 'right' : 'left'; + } + return dy > 0 ? dx > 0 ? 'down-right' : 'down-left' : dx > 0 ? 'up-right' : 'up-left'; + } else if (ddy > 0) { + if (ddx / ddy <= tan_Pi_div_8) { + return dy > 0 ? 'down' : 'up'; + } + return dx > 0 ? dy > 0 ? 'down-right' : 'up-left' : dy > 0 ? 'down-left' : 'up-left'; + } + return 'none'; +} + +VirtualJoystick.prototype._onUp = function() +{ + const dx = this._stickX - this._baseX; + const dy = this._stickY - this._baseY; + + switch (getCardinalDirection(dx, dy)) { + case 'up': + window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyK' })); + break; + case 'down': + window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyJ' })); + break; + case 'left': + window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyH' })); + break; + case 'right': + window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyL' })); + break; + case 'up-right': + window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyU'})); + break; + case 'up-left': + window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyY'})); + break; + case 'down-right': + window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyB'})); + break; + case 'down-left': + window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyN'})); + break; + default: + window.dispatchEvent(new KeyboardEvent('keydown', { code: 'Space'})); + break; + } + + this._pressed = false; + this._stickEl.style.display = "none"; + + if(this._stationaryBase == false){ + this._baseEl.style.display = "none"; + + this._baseX = this._baseY = 0; + this._stickX = this._stickY = 0; + } +} + +VirtualJoystick.prototype._onDown = function(x, y) +{ + this._pressed = true; + if(this._stationaryBase == false){ + this._baseX = x; + this._baseY = y; + this._baseEl.style.display = ""; + this._move(this._baseEl.style, (this._baseX - this._baseEl.width /2), (this._baseY - this._baseEl.height/2)); + } + + this._stickX = x; + this._stickY = y; + + if(this._limitStickTravel === true){ + var deltaX = this.deltaX(); + var deltaY = this.deltaY(); + var stickDistance = Math.sqrt( (deltaX * deltaX) + (deltaY * deltaY) ); + if(stickDistance > this._stickRadius){ + var stickNormalizedX = deltaX / stickDistance; + var stickNormalizedY = deltaY / stickDistance; + + this._stickX = stickNormalizedX * this._stickRadius + this._baseX; + this._stickY = stickNormalizedY * this._stickRadius + this._baseY; + } + } + + this._stickEl.style.display = ""; + this._move(this._stickEl.style, (this._stickX - this._stickEl.width /2), (this._stickY - this._stickEl.height/2)); +} + +VirtualJoystick.prototype._onMove = function(x, y) +{ + if( this._pressed === true ){ + this._stickX = x; + this._stickY = y; + + if(this._limitStickTravel === true){ + var deltaX = this.deltaX(); + var deltaY = this.deltaY(); + var stickDistance = Math.sqrt( (deltaX * deltaX) + (deltaY * deltaY) ); + if(stickDistance > this._stickRadius){ + var stickNormalizedX = deltaX / stickDistance; + var stickNormalizedY = deltaY / stickDistance; + + this._stickX = stickNormalizedX * this._stickRadius + this._baseX; + this._stickY = stickNormalizedY * this._stickRadius + this._baseY; + } + } + + this._move(this._stickEl.style, (this._stickX - this._stickEl.width /2), (this._stickY - this._stickEl.height/2)); + } +} + + +////////////////////////////////////////////////////////////////////////////////// +// bind touch events (and mouse events for debug) // +////////////////////////////////////////////////////////////////////////////////// + +VirtualJoystick.prototype._onMouseUp = function(event) +{ + return this._onUp(); +} + +VirtualJoystick.prototype._onMouseDown = function(event) +{ + event.preventDefault(); + var x = event.clientX; + var y = event.clientY; + return this._onDown(x, y); +} + +VirtualJoystick.prototype._onMouseMove = function(event) +{ + var x = event.clientX; + var y = event.clientY; + return this._onMove(x, y); +} + +////////////////////////////////////////////////////////////////////////////////// +// comment // +////////////////////////////////////////////////////////////////////////////////// + +VirtualJoystick.prototype._onTouchStart = function(event) +{ + // if there is already a touch inprogress do nothing + if( this._touchIdx !== null ) return; + + // notify event for validation + var isValid = this.dispatchEvent('touchStartValidation', event); + if( isValid === false ) return; + + // dispatch touchStart + this.dispatchEvent('touchStart', event); + + event.preventDefault(); + // get the first who changed + var touch = event.changedTouches[0]; + // set the touchIdx of this joystick + this._touchIdx = touch.identifier; + + // forward the action + var x = touch.pageX; + var y = touch.pageY; + return this._onDown(x, y) +} + +VirtualJoystick.prototype._onTouchEnd = function(event) +{ + // if there is no touch in progress, do nothing + if( this._touchIdx === null ) return; + + // dispatch touchEnd + this.dispatchEvent('touchEnd', event); + + // try to find our touch event + var touchList = event.changedTouches; + for(var i = 0; i < touchList.length && touchList[i].identifier !== this._touchIdx; i++); + // if touch event isnt found, + if( i === touchList.length) return; + + // reset touchIdx - mark it as no-touch-in-progress + this._touchIdx = null; + +//?????? +// no preventDefault to get click event on ios +event.preventDefault(); + + return this._onUp() +} + +VirtualJoystick.prototype._onTouchMove = function(event) +{ + // if there is no touch in progress, do nothing + if( this._touchIdx === null ) return; + + // try to find our touch event + var touchList = event.changedTouches; + for(var i = 0; i < touchList.length && touchList[i].identifier !== this._touchIdx; i++ ); + // if touch event with the proper identifier isnt found, do nothing + if( i === touchList.length) return; + var touch = touchList[i]; + + event.preventDefault(); + + var x = touch.pageX; + var y = touch.pageY; + return this._onMove(x, y) +} + + +////////////////////////////////////////////////////////////////////////////////// +// build default stickEl and baseEl // +////////////////////////////////////////////////////////////////////////////////// + +/** + * build the canvas for joystick base + */ +VirtualJoystick.prototype._buildJoystickBase = function() +{ + var canvas = document.createElement( 'canvas' ); + canvas.width = 126; + canvas.height = 126; + + var ctx = canvas.getContext('2d'); + ctx.beginPath(); + ctx.strokeStyle = this._strokeStyle; + ctx.lineWidth = 6; + ctx.arc( canvas.width/2, canvas.width/2, 40, 0, Math.PI*2, true); + ctx.stroke(); + + ctx.beginPath(); + ctx.strokeStyle = this._strokeStyle; + ctx.lineWidth = 2; + ctx.arc( canvas.width/2, canvas.width/2, 60, 0, Math.PI*2, true); + ctx.stroke(); + + return canvas; +} + +/** + * build the canvas for joystick stick + */ +VirtualJoystick.prototype._buildJoystickStick = function() +{ + var canvas = document.createElement( 'canvas' ); + canvas.width = 86; + canvas.height = 86; + var ctx = canvas.getContext('2d'); + ctx.beginPath(); + ctx.strokeStyle = this._strokeStyle; + ctx.lineWidth = 6; + ctx.arc( canvas.width/2, canvas.width/2, 40, 0, Math.PI*2, true); + ctx.stroke(); + return canvas; +} + +////////////////////////////////////////////////////////////////////////////////// +// move using translate3d method with fallback to translate > 'top' and 'left' +// modified from https://github.com/component/translate and dependents +////////////////////////////////////////////////////////////////////////////////// + +VirtualJoystick.prototype._move = function(style, x, y) +{ + if (this._transform) { + if (this._has3d) { + style[this._transform] = 'translate3d(' + x + 'px,' + y + 'px, 0)'; + } else { + style[this._transform] = 'translate(' + x + 'px,' + y + 'px)'; + } + } else { + style.left = x + 'px'; + style.top = y + 'px'; + } +} + +VirtualJoystick.prototype._getTransformProperty = function() +{ + var styles = [ + 'webkitTransform', + 'MozTransform', + 'msTransform', + 'OTransform', + 'transform' + ]; + + var el = document.createElement('p'); + var style; + + for (var i = 0; i < styles.length; i++) { + style = styles[i]; + if (null != el.style[style]) { + return style; + } + } +} + +VirtualJoystick.prototype._check3D = function() +{ + var prop = this._getTransformProperty(); + // IE8<= doesn't have `getComputedStyle` + if (!prop || !window.getComputedStyle) return module.exports = false; + + var map = { + webkitTransform: '-webkit-transform', + OTransform: '-o-transform', + msTransform: '-ms-transform', + MozTransform: '-moz-transform', + transform: 'transform' + }; + + // from: https://gist.github.com/lorenzopolidori/3794226 + var el = document.createElement('div'); + el.style[prop] = 'translate3d(1px,1px,1px)'; + document.body.insertBefore(el, null); + var val = getComputedStyle(el).getPropertyValue(map[prop]); + document.body.removeChild(el); + var exports = null != val && val.length && 'none' != val; + return exports; +}