Add explicit sound preview and streamline map chrome

This commit is contained in:
n30nex committed 2026-09-26 22:24:34 -04:00
1 parent 6e3f6cf367
commit 401a404b8e
7 files changed
+77 -1

No files matched your search

+2
View File
@@ -11,3 +11,5 @@ Target Canada 0.15.0, Worldwide 0.6.0 and Canada Android 1.1.0.
No public API changes. Existing topology and packet/audio clocks remain authoritative.
Builds, tests and browser artifacts run in GitHub Actions only.
The app supplies compact MapLibre canvas/gesture/attribution styling rather than shipping unused provider-control icons. The existing total byte limits remain unchanged.
+12
View File
@@ -322,6 +322,18 @@ export class RouteSonifier {
return notes.length;
}
/** Explicit preview gesture; does not enable or save live sound. */
async preview(): Promise<void> {
if (!this.supported() || this.paused) return;
const context = !this.context || this.context.state === 'closed' ? this.createContext() : this.context;
this.unlockMobileOutput(context);
if (context.state !== 'running') await context.resume();
if (context.state !== 'running') return;
this.setMasterLevel(MASTER_LEVEL * this.volume);
const scene = SCENES[this.scene];
this.schedule({ frequency: 440, startMS: 0, durationMS: Math.min(420,scene.maxDurationMS), brightness: Math.max(scene.minBrightness,1800*scene.brightnessScale), pan: 0, variation: 0, scene: this.scene, character: 'map' }, .8);
}
destroy(): void {
this.enabled = false;
this.stopActive();
+9
View File
@@ -50,3 +50,12 @@
.graph-focus select { min-height: 36px; background: var(--field-bg); color: var(--ink-strong); border: 0; }
.graph-focus small:empty { display: none; }
@media(max-width:900px) { .graph-focus { top: 106px; left: 8px; } }
:root[data-cinematic] .controls > :is(.layers-disclosure,.find-control,.sound-control,.popover-control), :root[data-cinematic] .display-disclosure { position:static; }
:root[data-cinematic] #layers-panel { position:absolute; top:auto; right:0; bottom:calc(100% + 10px); width:min(370px,calc(100vw - 24px)); max-height:calc(100dvh - 150px); }
:root[data-cinematic] .maplibregl-ctrl-bottom-right { opacity:1; }
.gpu-packet-canvas { z-index:2; }
.sound-preview-actions { display:flex; gap:8px; }
:root[data-cinematic] .director-enabled .node-inspector-sheet { visibility:visible; pointer-events:auto; }
@media(max-width:900px),(pointer:coarse) { :root[data-cinematic] #layers-panel { width:100%; max-height:calc(100dvh - 185px); } }
@media(max-height:520px) and (orientation:landscape) { :root[data-cinematic] #layers-panel { max-height:calc(100dvh - 95px); } }
+3 -1
View File
@@ -1,3 +1,4 @@
import { mountSoundPreview } from './soundPreview';
import { requestedNode } from './selection';
import { mountCinematicDock, mountLayerCombinations } from './cinematicChrome';
import { browserStorage } from './browserStorage';
@@ -6,7 +7,7 @@ import { mountDisplayControls } from './displayControls';
import { loadBasemapConfiguration } from './basemap';
import { attachMapNotice } from './mapNotice';
import { DEFAULT_DISPLAY, DISPLAY_EVENT, displayPreferences, initializeDisplay, updateDisplay, applyDisplayChrome } from './displayPreferences';
import 'maplibre-gl/dist/maplibre-gl.css';
import './maplibre-base.css';
import './styles.css';
import { fetchState, LiveFeed } from './api';
import { RouteSonifier, type SoundScene, type SoundStatus } from './audio';
@@ -252,6 +253,7 @@ async function start(): Promise<void> {
animator = liveAnimator;
const routeSonifier = new RouteSonifier(liveAnimator.projection, packetCanvas);
sonifier = routeSonifier;
mountSoundPreview(routeSonifier, soundPanel);
soundVolume.value = String(Math.round(routeSonifier.getVolume() * 100));
soundVolumeOutput.value = `${soundVolume.value}%`;
syncSoundScene(soundScene, routeSonifier.getScene());
+39
View File
@@ -0,0 +1,39 @@
/* CartoLite supplies its own controls and inspector. Only MapLibre's canvas,
gestures and attribution need library chrome (MapLibre GL JS 6.4.1 DOM). */
.maplibregl-map { position:relative; overflow:hidden; font:12px/20px system-ui,sans-serif; -webkit-tap-highlight-color:transparent; }
.maplibregl-canvas { position:absolute; left:0; top:0; }
.maplibregl-map:fullscreen { width:100%; height:100%; }
.maplibregl-interactive { cursor:grab; user-select:none; }
.maplibregl-interactive:active { cursor:grabbing; }
.maplibregl-track-pointer { cursor:pointer; }
.maplibregl-touch-zoom-rotate, .maplibregl-touch-zoom-rotate .maplibregl-canvas { touch-action:pan-x pan-y; }
.maplibregl-touch-drag-pan, .maplibregl-touch-drag-pan .maplibregl-canvas { touch-action:pinch-zoom; }
.maplibregl-touch-zoom-rotate.maplibregl-touch-drag-pan, .maplibregl-touch-zoom-rotate.maplibregl-touch-drag-pan .maplibregl-canvas { touch-action:none; }
.maplibregl-canvas:focus-visible { outline:2px solid var(--focus-ring); outline-offset:-2px; }
.maplibregl-ctrl-bottom-right,.maplibregl-ctrl-bottom-left,.maplibregl-ctrl-top-right,.maplibregl-ctrl-top-left { position:absolute; pointer-events:none; z-index:3; }
.maplibregl-ctrl-bottom-right { right:0; bottom:0; }
.maplibregl-ctrl-bottom-left { left:0; bottom:0; }
.maplibregl-ctrl-top-right { right:0; top:0; }
.maplibregl-ctrl-top-left { left:0; top:0; }
.maplibregl-ctrl { clear:both; pointer-events:auto; transform:translate(0,0); }
.maplibregl-ctrl-attrib { margin:0 6px 4px; padding:3px 8px; border-radius:6px; color:var(--ink); background:var(--panel-solid); font-size:10px; line-height:18px; max-width:calc(100vw - 12px); }
.maplibregl-ctrl-attrib a { color:inherit; text-decoration:none; }
.maplibregl-ctrl-attrib a:hover { text-decoration:underline; }
.maplibregl-ctrl-attrib-button { display:none; border:1px solid currentColor; border-radius:50%; background:var(--panel-solid); color:var(--ink); cursor:pointer; width:44px; height:44px; filter:none !important; }
.maplibregl-ctrl-attrib-button::before { content:'i'; font:bold 16px Georgia,serif; }
.maplibregl-compact { padding:0; min-height:44px; }
.maplibregl-compact .maplibregl-ctrl-attrib-button { display:block; float:right; }
.maplibregl-compact .maplibregl-ctrl-attrib-inner { display:none; }
.maplibregl-compact-show .maplibregl-ctrl-attrib-inner { display:block; padding:8px 48px 8px 10px; }
.maplibregl-ctrl-attrib.maplibregl-attrib-empty { display:none; }
.maplibregl-boxzoom { position:absolute; top:0; left:0; width:0; height:0; border:2px dashed var(--focus-ring); background:#72ffe118; }
.maplibregl-crosshair,.maplibregl-crosshair .maplibregl-interactive { cursor:crosshair; }
.maplibregl-popup { position:absolute; display:flex; top:0; left:0; pointer-events:none; }
.maplibregl-popup-content { position:relative; background:var(--panel-solid); border:1px solid var(--panel-border); border-radius:12px; pointer-events:auto; }
.maplibregl-popup-close-button { position:absolute; right:0; top:0; width:44px; height:44px; border:0; background:transparent; color:var(--ink); }
.maplibregl-popup-anchor-top,.maplibregl-popup-anchor-top-left,.maplibregl-popup-anchor-top-right { flex-direction:column; }
.maplibregl-popup-anchor-bottom,.maplibregl-popup-anchor-bottom-left,.maplibregl-popup-anchor-bottom-right { flex-direction:column-reverse; }
.maplibregl-popup-anchor-left { flex-direction:row; }
.maplibregl-popup-anchor-right { flex-direction:row-reverse; }
.maplibregl-popup-tip { display:none; }
@media(forced-colors:active) { .maplibregl-ctrl-attrib { border:1px solid CanvasText; } }
+2
View File
@@ -1,3 +1,4 @@
import { mountSoundPreview } from '../soundPreview';
import { requestedNode, rememberNode, replaceInspector } from '../selection';
import { mountCinematicDock } from '../cinematicChrome';
import { populateSoundScenes, syncSoundScene, SOUND_SCENES } from '../soundScenes';
@@ -107,6 +108,7 @@ async function start(): Promise<void> {
renderer = graph;
const routeSonifier = new RouteSonifier(graph, stage);
sonifier = routeSonifier;
mountSoundPreview(routeSonifier, soundPanel);
configureSound(routeSonifier);
graph.setRouteWindow(settings.routeWindow);
+10
View File
@@ -0,0 +1,10 @@
import type { RouteSonifier } from './audio';
export function mountSoundPreview(sonifier: RouteSonifier, parent: HTMLElement): void {
const controls = document.createElement('div'); controls.className = 'sound-preview-actions';
const preview = document.createElement('button'); preview.type='button'; preview.textContent='Preview voice'; preview.className='control-button';
preview.disabled=!sonifier.supported();
preview.addEventListener('click', async () => { preview.disabled=true; try { await sonifier.preview(); } catch { preview.textContent='Preview unavailable'; } finally { window.setTimeout(()=>{preview.disabled=!sonifier.supported();},700); } });
const mute=document.createElement('button'); mute.type='button';mute.textContent='Mute';mute.className='control-button';
mute.addEventListener('click',()=>{void sonifier.setEnabled(false);});
controls.append(preview,mute);parent.append(controls);
}