diff --git a/src/main.ts b/src/main.ts index 4dfe71c..4464856 100644 --- a/src/main.ts +++ b/src/main.ts @@ -23,7 +23,7 @@ import { nextWaveState, restingWave } from './sim/gravitationalWave'; import { clearBody, createWorld, placeBinary, placeBody, resetScene, stepWorld } from './sim/world'; import type { Body } from './sim/types'; import { CinematicMode, isTypingIntoControl } from './ui/chrome'; -import { isMobile, mobileRenderBudget } from './ui/device'; +import { touchRenderBudget, usesTouchUi } from './ui/device'; import { buildPanel } from './ui/panel'; import type { Preset } from './ui/presets'; import { PlacementController } from './ui/placement'; @@ -60,13 +60,13 @@ const hud = requireElement('hud'); const toast = requireElement('toast'); const chromeToggle = requireElement('chrome-toggle'); -// A finger-driven, small screen gets a different layout and a smaller render -// budget. Decided once at boot: a phone does not become a desktop mid-session, -// and re-laying-out the panel on every orientation change would be worse than -// the sizes being slightly off in landscape. -const mobile = isMobile(); -if (mobile) document.body.classList.add('touch-ui'); -const budget = mobileRenderBudget(); +// A finger-driven device gets the touch layout and a smaller render budget, +// phone or tablet alike. Decided once at boot: a device does not grow a mouse +// mid-session, and re-laying-out the panel on every orientation change would +// be worse than the sizes being slightly off in landscape. +const touchUi = usesTouchUi(); +if (touchUi) document.body.classList.add('touch-ui'); +const budget = touchRenderBudget(); const renderer = new THREE.WebGLRenderer({ antialias: false, @@ -76,14 +76,14 @@ if (!renderer.capabilities.isWebGL2) { hud.textContent = 'This visualizer needs WebGL2, which this browser does not provide.'; throw new Error('WebGL2 required'); } -renderer.setPixelRatio(Math.min(window.devicePixelRatio, mobile ? budget.pixelRatio : 2)); +renderer.setPixelRatio(Math.min(window.devicePixelRatio, touchUi ? budget.pixelRatio : 2)); app.appendChild(renderer.domElement); const settings = defaultSettings(); -if (mobile) settings.quality = 'low'; +if (touchUi) settings.quality = 'low'; const world = createWorld(); -const starfield = new Starfield(renderer, settings.sky, mobile ? budget.skyFaceSize : undefined); +const starfield = new Starfield(renderer, settings.sky, touchUi ? budget.skyFaceSize : undefined); const rig = new CameraRig(window.innerWidth / window.innerHeight, renderer.domElement); const bhPass = new BlackHolePass(starfield.texture, settings.quality); const pipeline = new RenderPipeline(renderer, rig.camera, bhPass, settings.quality); @@ -148,7 +148,7 @@ const aiming = new AimingController( const cinematic = new CinematicMode( document.body, toast, - mobile ? 'tap the button to bring them back' : 'press H for the controls', + touchUi ? 'tap the button to bring them back' : 'press H for the controls', ); chromeToggle.addEventListener('click', () => { cinematic.toggle(); @@ -246,7 +246,7 @@ const panel = buildPanel( rebakeSky(); }, }, - { debug: new URLSearchParams(window.location.search).has('debug'), compact: mobile }, + { debug: new URLSearchParams(window.location.search).has('debug'), compact: touchUi }, ); audio.setVolume(settings.volume); diff --git a/src/ui/device.ts b/src/ui/device.ts index da71144..fc7c1db 100644 --- a/src/ui/device.ts +++ b/src/ui/device.ts @@ -1,37 +1,32 @@ /** * What kind of device is this, in the only terms the app actually cares about. * - * Deliberately not "is it a phone". The questions worth asking are whether the - * pointer is a finger (so hover text and a keyboard shortcut are useless) and - * whether the screen is small enough that a 245px panel would swallow it. + * Deliberately not "is it a phone". The question that decides the layout is + * whether the primary pointer is a finger, because that is what makes hover + * text, keyboard shortcuts and 20px hit targets useless. */ -/** A finger or stylus rather than a mouse: no hover, no keyboard shortcuts. */ -export function hasCoarsePointer(): boolean { - return window.matchMedia('(pointer: coarse)').matches; -} - -/** Narrow enough that the control panel would cover most of the render. */ -export function hasCompactScreen(): boolean { - return Math.min(window.innerWidth, window.innerHeight) < 620; -} - /** - * Treat as mobile when both are true. A touchscreen laptop keeps the desktop - * layout, and a narrow desktop window keeps its keyboard shortcuts; only a - * device that is both small and finger-driven gets the phone treatment. + * True when the *primary* pointer is a finger or stylus. + * + * This is the whole test, on purpose. An earlier version also required a small + * screen, which left tablets on the desktop layout: a floating panel over a + * quarter of the screen, no way to hide the interface without a keyboard, and + * a desktop render budget on a mobile GPU. A touchscreen laptop is not caught + * by this, because with a mouse attached the primary pointer is fine; + * `any-pointer: coarse` would be the query that wrongly catches it. */ -export function isMobile(): boolean { - return hasCoarsePointer() && hasCompactScreen(); +export function usesTouchUi(): boolean { + return window.matchMedia('(pointer: coarse)').matches; } /** - * Render budget for this device. Phone GPUs are perhaps a tenth of a desktop - * card, and a phone screen reports a device pixel ratio of 3, so an - * unthrottled raymarch renders nine times the pixels on a tenth of the - * hardware. Cap the ratio and start at the cheapest preset; the existing - * auto-degrade handles anything still too slow. + * Render budget for a touch device. A phone or tablet GPU is a fraction of a + * desktop card while reporting a device pixel ratio of 2 or 3, so an + * unthrottled raymarch draws several times the pixels on a fraction of the + * hardware. Start cheap and let the existing auto-degrade take it further; the + * quality control is right there if the device turns out to have headroom. */ -export function mobileRenderBudget(): { pixelRatio: number; skyFaceSize: number } { +export function touchRenderBudget(): { pixelRatio: number; skyFaceSize: number } { return { pixelRatio: 1.25, skyFaceSize: 512 }; }