THE THUMBS: phones get their keys back
A touch strip for devices with no wheel and no keyboard — which is most of the phones the SHARE links land on. Each button IS its key: pointer down dispatches the real keydown, pointer up the real keyup, so the holds (crack charge, spoon drain) behave exactly like the physical key, and the heat arrows synthesise real wheel ticks at the canvas. Drags already worked — the input layer was pointer-events from birth. Only appears on touch devices (desktop cooks keep a clean bench; __tp() summons it for the curious), hides while the title is up, and the small- screen pass folds the rest: the title stacks and stops overflowing, the ledger fits, the judge's eye steps up out of the strip's way. Verified at 375x812: no horizontal scroll, buttons drive the real input path (heat arrows walked the poach knob 0 to 7 through the button handlers), pad hidden at title / shown in service, day-21 pot playable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
146c6f36c1
commit
9eb1c2db3a
@ -1,6 +1,7 @@
|
||||
import * as THREE from 'three';
|
||||
import { App, makeEnvironment, makeLights } from './core/app';
|
||||
import { audio } from './core/audio';
|
||||
import { installTouchpad } from './ui/touchpad';
|
||||
import { Game } from './game/game';
|
||||
import './style.css';
|
||||
|
||||
@ -40,4 +41,8 @@ window.addEventListener('keydown', (e) => {
|
||||
window.setTimeout(() => { w.style.opacity = '0'; }, 1800);
|
||||
});
|
||||
|
||||
installTouchpad(canvas);
|
||||
// Console cooks can summon the strip on desktop: __tp()
|
||||
(window as unknown as { __tp: () => void }).__tp = () => installTouchpad(canvas, true);
|
||||
|
||||
app.start();
|
||||
|
||||
@ -741,3 +741,62 @@ body.shadow .load {
|
||||
letter-spacing: 0.06em;
|
||||
margin: 2px 0 6px;
|
||||
}
|
||||
|
||||
/* The touch strip - phones get their keys back. */
|
||||
#touchpad {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
z-index: 35;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
.tp-btn {
|
||||
min-width: 44px;
|
||||
height: 44px;
|
||||
padding: 0 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #5a4a38;
|
||||
background: rgba(30, 24, 18, 0.82);
|
||||
color: #e8dcc4;
|
||||
font: 700 14px ui-monospace, monospace;
|
||||
touch-action: none;
|
||||
}
|
||||
.tp-btn:active {
|
||||
background: #6a5335;
|
||||
}
|
||||
.tp-wide { padding: 0 14px; }
|
||||
@media (min-width: 900px) and (hover: hover) {
|
||||
#touchpad { display: none; } /* desktops with a mouse keep the clean bench */
|
||||
}
|
||||
|
||||
/* Small screens: the bench folds to fit the phone. */
|
||||
#touchpad {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
max-width: 100vw;
|
||||
padding: 0 4px;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
#judge-eye {
|
||||
bottom: 104px !important;
|
||||
width: 60px !important;
|
||||
height: 60px !important;
|
||||
}
|
||||
.title-wrap {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.title-txt h1 { font-size: 44px; }
|
||||
.title-art { max-width: 55vw; }
|
||||
.title-txt { max-width: 94vw; }
|
||||
.title-days { max-width: 94vw; }
|
||||
}
|
||||
|
||||
/* No keys while you read the menu - the title disposes itself on start. */
|
||||
body:has(#ui > .title) #touchpad { display: none; }
|
||||
|
||||
50
src/ui/touchpad.ts
Normal file
50
src/ui/touchpad.ts
Normal file
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* The touch strip: phones have no wheel and no keyboard, and half the kitchen
|
||||
* runs on both. Each button IS its key — pointer down dispatches keydown,
|
||||
* pointer up dispatches keyup, so holds (the crack charge, the drain) work
|
||||
* exactly like the physical key. The heat arrows synthesise wheel ticks.
|
||||
* Only appears on touch devices; the buttons never steal from the canvas.
|
||||
*/
|
||||
export function installTouchpad(canvas: HTMLCanvasElement, force = false): void {
|
||||
if (!force && !('ontouchstart' in window) && navigator.maxTouchPoints === 0) return;
|
||||
const pad = document.createElement('div');
|
||||
pad.id = 'touchpad';
|
||||
document.body.appendChild(pad);
|
||||
|
||||
const key = (label: string, code: string, wide = false) => {
|
||||
const b = document.createElement('button');
|
||||
b.className = `tp-btn${wide ? ' tp-wide' : ''}`;
|
||||
b.textContent = label;
|
||||
b.addEventListener('pointerdown', (e) => {
|
||||
e.preventDefault();
|
||||
b.setPointerCapture(e.pointerId);
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { code }));
|
||||
});
|
||||
const up = () => window.dispatchEvent(new KeyboardEvent('keyup', { code }));
|
||||
b.addEventListener('pointerup', up);
|
||||
b.addEventListener('pointercancel', up);
|
||||
pad.appendChild(b);
|
||||
return b;
|
||||
};
|
||||
const wheelBtn = (label: string, dir: 1 | -1) => {
|
||||
const b = document.createElement('button');
|
||||
b.className = 'tp-btn';
|
||||
b.textContent = label;
|
||||
b.addEventListener('pointerdown', (e) => {
|
||||
e.preventDefault();
|
||||
canvas.dispatchEvent(new WheelEvent('wheel', { deltaY: dir * 100, bubbles: true, cancelable: true }));
|
||||
});
|
||||
pad.appendChild(b);
|
||||
};
|
||||
|
||||
wheelBtn('▼', -1); // heat down
|
||||
wheelBtn('▲', 1); // heat up
|
||||
key('←', 'ArrowLeft');
|
||||
key('→', 'ArrowRight');
|
||||
key('B', 'KeyB');
|
||||
key('F', 'KeyF');
|
||||
key('D', 'KeyD');
|
||||
key('L', 'KeyL');
|
||||
key('HOLD ␣', 'Space', true);
|
||||
key('SERVE', 'Enter', true);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user