Mobile: touch controls with visual feedback, landscape support, larger hit areas
This commit is contained in:
+215
-104
@@ -2,36 +2,41 @@
|
|||||||
<html lang="it">
|
<html lang="it">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||||
<title>A Scream from the Dark — WebAssembly Game Boy</title>
|
<title>A Scream from the Dark — WebAssembly Game Boy</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
background-color: #0a0a0f;
|
background-color: #0a0a0f;
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
|
overflow-x: hidden;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 100vh;
|
padding: 16px;
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
color: #c41e3a;
|
color: #c41e3a;
|
||||||
text-shadow: 2px 2px 0 #000, 0 0 20px rgba(196, 30, 58, 0.5);
|
text-shadow: 2px 2px 0 #000, 0 0 20px rgba(196, 30, 58, 0.5);
|
||||||
font-size: 2em;
|
font-size: 1.6em;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
margin: 0 0 8px 0;
|
margin: 0 0 6px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
color: #8b0000;
|
color: #8b0000;
|
||||||
font-size: 0.85em;
|
font-size: 0.75em;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
margin: 0 0 24px 0;
|
margin: 0 0 16px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
canvas {
|
canvas {
|
||||||
@@ -40,6 +45,7 @@
|
|||||||
image-rendering: crisp-edges;
|
image-rendering: crisp-edges;
|
||||||
width: 320px;
|
width: 320px;
|
||||||
height: 288px;
|
height: 288px;
|
||||||
|
max-width: 100%;
|
||||||
border: 4px solid #2a2a2a;
|
border: 4px solid #2a2a2a;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 0 30px rgba(196, 30, 58, 0.3),
|
box-shadow: 0 0 30px rgba(196, 30, 58, 0.3),
|
||||||
@@ -47,11 +53,11 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.controls {
|
.controls {
|
||||||
margin-top: 20px;
|
margin-top: 16px;
|
||||||
background: #1a1a1a;
|
background: #1a1a1a;
|
||||||
padding: 12px 20px;
|
padding: 10px 16px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 1px solid #2a2a2a;
|
border: 1px solid #2a2a2a;
|
||||||
@@ -65,56 +71,126 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border: 1px solid #3a3a3a;
|
border: 1px solid #3a3a3a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
MOBILE / TOUCH CONTROLS
|
||||||
|
Visible on screens narrower than 700px OR
|
||||||
|
when device has coarse pointer (touch).
|
||||||
|
============================================ */
|
||||||
#mobile-controls {
|
#mobile-controls {
|
||||||
display: none;
|
display: none;
|
||||||
margin-top: 20px;
|
margin-top: 18px;
|
||||||
gap: 20px;
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
touch-action: none;
|
||||||
}
|
}
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 700px), (pointer: coarse) {
|
||||||
#mobile-controls { display: flex; }
|
#mobile-controls { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 0 8px; }
|
||||||
.controls { display: none; }
|
.controls { display: none; }
|
||||||
}
|
}
|
||||||
.btn {
|
|
||||||
background: #1a1a1a;
|
/* Base touch button */
|
||||||
|
.tbtn {
|
||||||
|
background: linear-gradient(180deg, #2a2a2a 0%, #1a1a1a 100%);
|
||||||
color: #c41e3a;
|
color: #c41e3a;
|
||||||
border: 2px solid #2a2a2a;
|
border: 2px solid #3a3a3a;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 14px;
|
font-size: 18px;
|
||||||
touch-action: manipulation;
|
touch-action: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
-webkit-user-select: none;
|
||||||
.btn:active { background: #2a2a2a; }
|
|
||||||
.dpad {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 50px 50px 50px;
|
|
||||||
grid-template-rows: 50px 50px 50px;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
.dpad .btn { border-radius: 6px; }
|
|
||||||
.d-up { grid-column: 2; grid-row: 1; }
|
|
||||||
.d-left { grid-column: 1; grid-row: 2; }
|
|
||||||
.d-right { grid-column: 3; grid-row: 2; }
|
|
||||||
.d-down { grid-column: 2; grid-row: 3; }
|
|
||||||
.action-btns {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.05s, background 0.05s, border-color 0.05s, box-shadow 0.05s;
|
||||||
|
box-shadow: 0 3px 0 #000, inset 0 1px 0 rgba(255,255,255,0.05);
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.tbtn.pressed,
|
||||||
|
.tbtn:active,
|
||||||
|
.tbtn.active {
|
||||||
|
background: linear-gradient(180deg, #c41e3a 0%, #8b0000 100%);
|
||||||
|
color: #fff;
|
||||||
|
border-color: #c41e3a;
|
||||||
|
transform: translateY(2px);
|
||||||
|
box-shadow: 0 1px 0 #000, inset 0 1px 0 rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* D-pad: cross layout, larger buttons for thumbs */
|
||||||
|
.dpad {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 60px 60px 60px;
|
||||||
|
grid-template-rows: 60px 60px 60px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.dpad .tbtn {
|
||||||
|
border-radius: 50% / 30%;
|
||||||
|
}
|
||||||
|
.d-up { grid-column: 2; grid-row: 1; }
|
||||||
|
.d-left { grid-column: 1; grid-row: 2; }
|
||||||
|
.d-right { grid-column: 3; grid-row: 2; }
|
||||||
|
.d-down { grid-column: 2; grid-row: 3; }
|
||||||
|
.d-center {
|
||||||
|
grid-column: 2; grid-row: 2;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A/B action buttons */
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
.btn-round {
|
.actions .tbtn {
|
||||||
width: 50px;
|
width: 64px;
|
||||||
height: 50px;
|
height: 64px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
.btn-pill {
|
.btn-b { transform: rotate(-15deg); }
|
||||||
padding: 8px 14px;
|
.btn-b.pressed, .btn-b:active { transform: rotate(-15deg) translateY(2px); }
|
||||||
border-radius: 20px;
|
.btn-a { transform: rotate(15deg); }
|
||||||
font-size: 12px;
|
.btn-a.pressed, .btn-a:active { transform: rotate(15deg) translateY(2px); }
|
||||||
}
|
|
||||||
.start-select {
|
/* Start / Select pill buttons (top center) */
|
||||||
|
.sys {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.sys .tbtn {
|
||||||
|
min-width: 78px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 14px;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
padding: 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fullscreen & helpers */
|
||||||
|
.btn-fs {
|
||||||
|
margin-top: 12px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #c41e3a;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.btn-fs:hover { background: #2a2a2a; color: #fff; }
|
||||||
|
|
||||||
#game-container {
|
#game-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -127,39 +203,31 @@
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
background-color: #0a0a0f;
|
background-color: #0a0a0f;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-sizing: border-box;
|
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
#game-container:fullscreen canvas {
|
#game-container:fullscreen canvas {
|
||||||
width: 80vmin;
|
width: 80vmin;
|
||||||
height: 72vmin;
|
height: 72vmin;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 75vh;
|
max-height: 70vh;
|
||||||
}
|
|
||||||
.btn-fs {
|
|
||||||
margin-top: 15px;
|
|
||||||
background: #1a1a1a;
|
|
||||||
color: #c41e3a;
|
|
||||||
border: 1px solid #2a2a2a;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
.btn-fs:hover {
|
|
||||||
background: #2a2a2a;
|
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
margin-top: 30px;
|
margin-top: 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #555;
|
color: #555;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
.footer a { color: #c41e3a; text-decoration: none; }
|
.footer a { color: #c41e3a; text-decoration: none; }
|
||||||
.footer a:hover { text-decoration: underline; }
|
.footer a:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
/* Landscape: compact controls to leave room for screen */
|
||||||
|
@media (orientation: landscape) and (max-height: 500px) {
|
||||||
|
h1 { font-size: 1.1em; margin-bottom: 2px; }
|
||||||
|
h2 { display: none; }
|
||||||
|
canvas { height: 70vh; width: auto; }
|
||||||
|
#mobile-controls { margin-top: 8px; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -173,19 +241,25 @@
|
|||||||
<button class="btn-fs" id="btn-fullscreen">⛶ Schermo Intero</button>
|
<button class="btn-fs" id="btn-fullscreen">⛶ Schermo Intero</button>
|
||||||
|
|
||||||
<div id="mobile-controls">
|
<div id="mobile-controls">
|
||||||
<div class="dpad">
|
<!-- D-pad (sinistra) -->
|
||||||
<button class="btn d-up" id="btn-up">▲</button>
|
<div class="dpad" aria-label="Directional pad">
|
||||||
<button class="btn d-left" id="btn-left">◀</button>
|
<button class="tbtn d-up" data-key="ArrowUp" aria-label="Su">▲</button>
|
||||||
<button class="btn d-right" id="btn-right">▶</button>
|
<button class="tbtn d-left" data-key="ArrowLeft" aria-label="Sinistra">◀</button>
|
||||||
<button class="btn d-down" id="btn-down">▼</button>
|
<div class="d-center"></div>
|
||||||
|
<button class="tbtn d-right" data-key="ArrowRight" aria-label="Destra">▶</button>
|
||||||
|
<button class="tbtn d-down" data-key="ArrowDown" aria-label="Giù">▼</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="start-select">
|
|
||||||
<button class="btn btn-pill" id="btn-select">SELECT</button>
|
<!-- Start / Select (centro) -->
|
||||||
<button class="btn btn-pill" id="btn-start">START</button>
|
<div class="sys" aria-label="System buttons">
|
||||||
|
<button class="tbtn" data-key="Enter" id="btn-start" aria-label="Start">START</button>
|
||||||
|
<button class="tbtn" data-key=" " id="btn-select" aria-label="Select">SELECT</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-btns">
|
|
||||||
<button class="btn btn-round" id="btn-b">B</button>
|
<!-- A / B (destra, in stile GB inclinati) -->
|
||||||
<button class="btn btn-round" id="btn-a">A</button>
|
<div class="actions" aria-label="Action buttons">
|
||||||
|
<button class="tbtn btn-b" data-key="x" id="btn-b" aria-label="B (corsa)">B</button>
|
||||||
|
<button class="tbtn btn-a" data-key="z" id="btn-a" aria-label="A (salto)">A</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -226,37 +300,74 @@
|
|||||||
console.error('Errore:', err);
|
console.error('Errore:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
// WasmBoy native: Z=A, X=B, Enter=Start, Space=Select
|
/* ============================================
|
||||||
const bindButton = (id, keyDef) => {
|
Universal button binder (data-key driven)
|
||||||
const btn = document.getElementById(id);
|
- Touch: synthetic keydown/keyup events
|
||||||
const trigger = (type) => (e) => {
|
- Mouse: same
|
||||||
e.preventDefault();
|
- Visual feedback: .pressed class
|
||||||
let ev;
|
- Multi-touch: each button tracks its own state
|
||||||
try {
|
============================================ */
|
||||||
ev = new KeyboardEvent(type, { key: keyDef.key, code: keyDef.code, bubbles: true, cancelable: true });
|
const KEY_CODE = {
|
||||||
} catch (e) {
|
'ArrowUp':'ArrowUp','ArrowDown':'ArrowDown','ArrowLeft':'ArrowLeft','ArrowRight':'ArrowRight',
|
||||||
ev = document.createEvent('KeyboardEvent');
|
'z':'KeyZ','Z':'KeyZ',
|
||||||
ev.initKeyboardEvent(type, true, true, window, keyDef.key, 0, false, false, false, false);
|
'x':'KeyX','X':'KeyX',
|
||||||
}
|
'Enter':'Enter','enter':'Enter',
|
||||||
document.dispatchEvent(ev);
|
' ':'Space'
|
||||||
};
|
};
|
||||||
btn.addEventListener('touchstart', trigger('keydown'), { passive: false });
|
const KEY_KEY = {
|
||||||
btn.addEventListener('touchend', trigger('keyup'), { passive: false });
|
'ArrowUp':'ArrowUp','ArrowDown':'ArrowDown','ArrowLeft':'ArrowLeft','ArrowRight':'ArrowRight',
|
||||||
btn.addEventListener('mousedown', trigger('keydown'));
|
'z':'z','Z':'z',
|
||||||
btn.addEventListener('mouseup', trigger('keyup'));
|
'x':'x','X':'x',
|
||||||
btn.addEventListener('mouseleave', trigger('keyup'));
|
'Enter':'Enter','enter':'Enter',
|
||||||
|
' ':' '
|
||||||
};
|
};
|
||||||
|
|
||||||
bindButton('btn-up', { key: 'ArrowUp', code: 'ArrowUp' });
|
function dispatchKey(type, dataKey) {
|
||||||
bindButton('btn-down', { key: 'ArrowDown', code: 'ArrowDown' });
|
const ev = new KeyboardEvent(type, {
|
||||||
bindButton('btn-left', { key: 'ArrowLeft', code: 'ArrowLeft' });
|
key: KEY_KEY[dataKey] || dataKey,
|
||||||
bindButton('btn-right', { key: 'ArrowRight', code: 'ArrowRight' });
|
code: KEY_CODE[dataKey] || dataKey,
|
||||||
bindButton('btn-a', { key: 'z', code: 'KeyZ' });
|
bubbles: true,
|
||||||
bindButton('btn-b', { key: 'x', code: 'KeyX' });
|
cancelable: true,
|
||||||
bindButton('btn-start', { key: 'Enter', code: 'Enter' });
|
composed: true
|
||||||
bindButton('btn-select',{ key: ' ', code: 'Space' });
|
});
|
||||||
|
document.dispatchEvent(ev);
|
||||||
|
}
|
||||||
|
|
||||||
// Fullscreen toggle
|
document.querySelectorAll('.tbtn[data-key]').forEach(btn => {
|
||||||
|
const dataKey = btn.dataset.key;
|
||||||
|
let pressed = false;
|
||||||
|
const press = (e) => {
|
||||||
|
if (e) { e.preventDefault(); e.stopPropagation(); }
|
||||||
|
if (pressed) return;
|
||||||
|
pressed = true;
|
||||||
|
btn.classList.add('pressed');
|
||||||
|
dispatchKey('keydown', dataKey);
|
||||||
|
};
|
||||||
|
const release = (e) => {
|
||||||
|
if (e) { e.preventDefault(); e.stopPropagation(); }
|
||||||
|
if (!pressed) return;
|
||||||
|
pressed = false;
|
||||||
|
btn.classList.remove('pressed');
|
||||||
|
dispatchKey('keyup', dataKey);
|
||||||
|
};
|
||||||
|
// Touch (primary)
|
||||||
|
btn.addEventListener('touchstart', press, { passive: false });
|
||||||
|
btn.addEventListener('touchend', release, { passive: false });
|
||||||
|
btn.addEventListener('touchcancel',release, { passive: false });
|
||||||
|
// Mouse (fallback per emulatori desktop e testing)
|
||||||
|
btn.addEventListener('mousedown', press);
|
||||||
|
btn.addEventListener('mouseup', release);
|
||||||
|
btn.addEventListener('mouseleave', release);
|
||||||
|
// Keyboard accessibility (Enter/Space su button = click = simulate tap)
|
||||||
|
btn.addEventListener('click', e => e.preventDefault()); // niente click reale
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent default for arrow keys (avoid page scroll on Space/Arrows)
|
||||||
|
window.addEventListener('keydown', e => {
|
||||||
|
if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key)) e.preventDefault();
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
/* Fullscreen toggle */
|
||||||
const gameContainer = document.getElementById('game-container');
|
const gameContainer = document.getElementById('game-container');
|
||||||
const fsButton = document.getElementById('btn-fullscreen');
|
const fsButton = document.getElementById('btn-fullscreen');
|
||||||
fsButton.addEventListener('click', () => {
|
fsButton.addEventListener('click', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user