diff --git a/src/core/app.ts b/src/core/app.ts index 0305339..e43c64f 100644 --- a/src/core/app.ts +++ b/src/core/app.ts @@ -147,6 +147,25 @@ export class App { const h = Math.max(1, window.innerHeight); this.renderer.setSize(w, h, false); this.camera.aspect = w / h; + // Every scene is composed left-to-right — the spice rack, the coal bed, the + // drawer of cutlery. A perspective camera holds its VERTICAL fov, so a + // portrait phone silently crops the sides off all of it: on a 375x812 + // screen the outer spice jars were simply not on the display, and you + // cannot drag a jar you cannot see. So below 1:1 we widen the vertical fov + // to hold the HORIZONTAL extent the scenes were framed for instead. + const DESIGN_ASPECT = 16 / 9; + const BASE_FOV = 42; + // Capped, because holding the FULL 16:9 width on a 0.46 phone needs 112 + // degrees, and that is not a camera, it is a peephole — the bench went + // small and distant in the middle of an empty floor. 74 gets the outer + // props back on screen while still looking like a lens. + const MAX_FOV = 74; + if (this.camera.aspect < DESIGN_ASPECT) { + const halfW = Math.tan((BASE_FOV * Math.PI) / 360) * DESIGN_ASPECT; + this.camera.fov = Math.min(MAX_FOV, (Math.atan(halfW / this.camera.aspect) * 360) / Math.PI); + } else { + this.camera.fov = BASE_FOV; + } this.camera.updateProjectionMatrix(); this.view?.resize?.(w, h); } diff --git a/src/game/game.ts b/src/game/game.ts index c8b64e2..2c6e4d2 100644 --- a/src/game/game.ts +++ b/src/game/game.ts @@ -185,6 +185,13 @@ export class Game { this.ticket = new Panel('ticket'); this.ticketEl = el('div', 'ticket-body', this.ticket.root); + // On a phone the ticket covers half the bench — and you cannot drag a + // spice jar you cannot reach. Tap it to fold it down to the day and the + // customer; tap again to read the whole ask. Desktop ignores this. + this.ticket.root.addEventListener('click', (e) => { + if ((e.target as HTMLElement).tagName === 'BUTTON') return; // the demo button still works + if (window.matchMedia('(max-width: 600px)').matches) this.ticket.root.classList.toggle('folded'); + }); this.tempPanel = new Panel('temp-toggle'); this.tempPanel.hide(); @@ -1298,6 +1305,7 @@ export class Game { } private renderTicket(): void { + this.ticket.root.classList.remove('folded'); const o = this.order; this.ticketEl.innerHTML = ''; el('div', 'ticket-day', this.ticketEl, `DAY ${o.day}`); diff --git a/src/style.css b/src/style.css index 451c228..5d3f6b6 100644 --- a/src/style.css +++ b/src/style.css @@ -895,3 +895,51 @@ body:has(#ui > .title) #touchpad { display: none; } background: #f0e2c0; border-radius: 1px; } + +/* Small screens: the ask and the live words have to SHARE 375px, and both + were still desktop width — the card landed straight back under the ticket + on every phone. Narrow both, shrink the type, and cap the ticket's height + so a chatty customer cannot push the card off the bottom of the world. */ +@media (max-width: 600px) { + .ticket { + width: 50vw; + padding: 10px 11px 9px; + font-size: 11px; + max-height: 46vh; + overflow-y: auto; + } + .ticket-face { + width: 34px; + height: 34px; + top: 7px; + right: 8px; + } + .slicer-card { + width: 41vw; + top: 12px; + right: 10px; + padding: 10px 11px; + } + .slicer-ask { font-size: 11px; } + .slicer-thick, + .slicer-wobble { font-size: 10px; } + .drawer-hint { font-size: 9px; } + .taste-axis { width: 34px; font-size: 8px; } +} + +/* Folded ticket (phones): the day, who is asking, and the demo button. The + rest is one tap away, and the bench underneath is reachable again. */ +@media (max-width: 600px) { + .ticket { cursor: pointer; } + .ticket.folded { max-height: none; } + .ticket.folded .ticket-body > *:not(.ticket-day):not(.ticket-who):not(.ticket-face):not(.watch-btn):not(.ticket-critic) { + display: none; + } + .ticket.folded::after { + content: 'tap to read'; + display: block; + margin-top: 4px; + font: 9px ui-monospace, monospace; + opacity: 0.5; + } +}