nordic-skating / commit
feat: a Tilt button, and a screen that stays put while tilt steers
index.html | 4 +++- src/main.ts | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- src/style.css | 26 ++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index d32e685..09cbfba 100644 --- a/index.html +++ b/index.html @@ -24,6 +24,8 @@ <button id="randomize" type="button">Randomize</button> </nav> <button id="sound" type="button"></button> +<!-- Shown only on what can be tilted (main.ts). --> +<button id="tilt-button" type="button" hidden></button> <dialog id="about-dialog" aria-labelledby="about-title"> <h1 id="about-title" tabindex="-1" autofocus>Nordic Skating</h1> <p>An endless skate over thin black ice. The ice sings under every stride, and the thinner it is, the higher it sings. Leave it alone and it skates itself, for as long as you like.</p> @@ -31,7 +33,7 @@ <h2>Controls</h2> <dl> <dt>Steer</dt> - <dd>Drag left or right from wherever you touch, or use ← → or A D; or, on a phone, turn on Tilt to steer in Settings and lean it. Let go and the skater straightens up, then drifts back to its own line. Hold it over and the skater turns right round: there is room for a circle wherever the water is wide.</dd> + <dd>Drag left or right from wherever you touch, or use ← → or A D; or, on a phone, press Tilt at the bottom left and lean it. Let go and the skater straightens up, then drifts back to its own line. Hold it over and the skater turns right round: there is room for a circle wherever the water is wide.</dd> <dt>Choose a way at a fork</dt> <dd>A touch towards it on the way up to the fork is enough. Do nothing and the skater picks one.</dd> <dt>Islets</dt> diff --git a/src/main.ts b/src/main.ts index 2f02cc3..e4d000f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -182,21 +182,68 @@ for (const [button, box] of [[aboutButton, about], [settingsButton, settingsBox] const impossible = settingsBox.querySelector<HTMLInputElement>('#impossible')! const tilt = settingsBox.querySelector<HTMLInputElement>('#tilt')! const tiltRefused = settingsBox.querySelector<HTMLElement>('#tilt-refused')! -// Steering by tilt is offered only on what can be tilted. +const tiltButton = document.querySelector<HTMLButtonElement>('#tilt-button')! +// Steering by tilt is offered only on what can be tilted: in the settings, and by a +// button of its own at the foot of the screen, which is the quick way to it. settingsBox.querySelector<HTMLElement>('#steering')!.hidden = !Input.canTilt +tiltButton.hidden = !Input.canTilt +// A touch of the button is the touch an iPhone wants before it will give its leave. +tiltButton.addEventListener('click', () => void setTilt(!settings.tilt)) +/** Whether the page was put into full screen in order to hold the screen's rotation. */ +let wentFullScreen = false +/** + * Stop the screen turning with the phone while tilt steers, and let it turn again after. + * Leaning a phone over to steer is exactly what makes its screen flip to the other + * orientation, and the lake with it. + * + * A browser holds the rotation only for a page that fills the screen: an installed app + * already does, and in a browser tab the page is put into full screen to get it, and + * taken out again when tilt is turned off. Where the browser will do neither, as on an + * iPhone, nothing happens, and the phone's own rotation lock is the way. + */ +async function holdRotation(hold: boolean): Promise<void> { + const turning = screen.orientation as (ScreenOrientation & { lock?: (to: string) => Promise<void> }) | undefined + try { + if (!hold) { + turning?.unlock?.() + if (wentFullScreen && document.fullscreenElement) await document.exitFullscreen() + wentFullScreen = false + return + } + if (!turning?.lock) return + try { + await turning.lock(turning.type) + } catch { + if (!document.fullscreenElement) { + await document.documentElement.requestFullscreen() + wentFullScreen = true + } + await turning.lock(turning.type) + } + } catch { + // Not allowed here, or not without a touch: the screen turns as it always did. + } +} + /** Turn steering by tilt on or off, and say so if the browser will not have it. */ async function setTilt(on: boolean): Promise<void> { const given = on && (await input.tiltOn()) if (!given) input.tiltOff() tiltRefused.hidden = !on || given + // Refused from the button, there is nowhere to say so but the settings: open them there. + if (on && !given && !settingsBox.open) settingsButton.click() settings.tilt = given saveSettings(settings) showSettings() + void holdRotation(given) } // Remembered from last time. An iPhone gives its leave afresh each visit and only at a // touch, so there it is asked for at the first one. if (settings.tilt && Input.canTilt) { - if (Input.tiltNeedsTouch) window.addEventListener('pointerdown', () => void setTilt(true), { once: true }) + // (Unless that touch is of the Tilt button, which is someone turning it off.) + if (Input.tiltNeedsTouch) { + window.addEventListener('pointerdown', (e) => void (e.target !== tiltButton && setTilt(true)), { once: true }) + } else void setTilt(true) } const impossibleOnly = document.querySelectorAll<HTMLElement>('.impossible-only') @@ -206,6 +253,9 @@ function showSettings(): void { } impossible.checked = settings.impossibleIce tilt.checked = settings.tilt + tiltButton.textContent = settings.tilt ? '📱 Tilt on' : '📱 Tilt off' + tiltButton.setAttribute('aria-pressed', String(settings.tilt)) + tiltButton.classList.toggle('on', settings.tilt) hint.textContent = settings.tilt ? 'Tilt to steer — or just watch' : 'Drag left or right to steer — or just watch' // What the About box says of impossible ice is there only when the ice is. for (const part of impossibleOnly) part.hidden = !settings.impossibleIce diff --git a/src/style.css b/src/style.css index 63b1560..6a96409 100644 --- a/src/style.css +++ b/src/style.css @@ -242,3 +242,29 @@ dialog button.done { font-size: 0.9rem; } } + +/* Steering by tilt, on or off: the sound button's smaller cousin, at the foot of the screen. */ +#tilt-button { + position: fixed; + left: calc(0.75rem + env(safe-area-inset-left)); + bottom: calc(0.75rem + env(safe-area-inset-bottom)); + min-height: 2.75rem; + padding: 0 0.9rem; + border: 2px solid #fff; + border-radius: 2rem; + background: rgb(8 24 34 / 85%); + color: #fff; + font: 600 0.95rem/1 system-ui, sans-serif; + touch-action: manipulation; + cursor: pointer; +} + +#tilt-button.on { + background: #fff; + color: #06202c; +} + +/* The hint keeps clear of it. */ +#tilt-button:not([hidden]) ~ #hint { + bottom: calc(4.5rem + env(safe-area-inset-bottom)); +}