Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);

Expand Down
43 changes: 19 additions & 24 deletions src/ui/device.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
Loading