fuzzyworld

tower / commit

feat(ui): show or hide every label, from the panel or with L

1c0284d08306589aeec17f03139aa684ba51748c · Pavel Safronov · 2026-09-27 16:40:17 -0700 · parents 2edbf4728a96

Claude Code usage: this commit ≈0 out / ≈0 cached · cost unknown (no per-model trailer) · session 2 tally 744 in / 324.4K out / 79.7M cached · usage by commit and session

feat(ui): show or hide every label, from the panel or with L

One Show labels switch in the View section, and the L key in any view,
hides the map's names and captions and the exterior and interior captions.
The choice is remembered across reloads.

Usage: 744 in / 324.4K out / 79.7M cached (session tally)
rtk:   905.3K saved, 15.2% over 2657 cmds (global/lifetime)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
 README.md        |  4 +++-
 index.html       |  4 +++-
 src/style.css    |  2 ++
 src/ui/app.ts    | 25 +++++++++++++++++++++++++
 src/views/map.ts |  8 +++++++-
 5 files changed, 40 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index 7301248..6c1b31d 100644
--- a/README.md
+++ b/README.md
@@ -241,7 +241,9 @@ interior — to see its detail card: its name, its blurb, where it is, and what
 made of it — a range's named summits, the sea running on a lake or sea *now* by the
 clock, how many trees a forest holds and how far apart, what a desert's landforms came
 to. **Shift-click two points** on the map to measure a sightline (Esc clears it), and
-**F** fits the map.
+**F** fits the map. **Show labels** in the View section — or **L**, in any view — hides
+or shows every label at once: the map's place names and captions, and the captions over
+the exterior and interior. The choice is remembered across reloads.
 
 Changes to the world — a new place, a moved summit, a storm in the weather plan — are
 made in `content/world.json`, not in the browser (D-072).
diff --git a/index.html b/index.html
index 2e45593..ca32cac 100644
--- a/index.html
+++ b/index.html
@@ -40,12 +40,14 @@
     <details class="sec" id="view-section" open>
       <summary>View</summary>
       <label class="row" id="terrain-row"><input id="f-terrain" type="checkbox" checked /> Show terrain</label>
+      <label class="row"><input id="f-labels" type="checkbox" checked /> Show labels <span class="dim">— L</span></label>
       <div id="map-options">
         <p class="hint">Generated live from the world's features, in your browser.
           On the exterior it is wrapped onto the hull — an x-ray of the inner
           surface, not what Tower looks like from outside.</p>
         <p class="hint">Click a place to select it. <b>Shift-click two points</b> to
-          measure a sightline between them; Esc clears it. <b>F</b> fits the map.</p>
+          measure a sightline between them; Esc clears it. <b>F</b> fits the map,
+          and <b>L</b> shows or hides the labels in any view.</p>
       </div>
       <div id="exterior-options" class="hidden">
         <label>Hull
diff --git a/src/style.css b/src/style.css
index 2d45921..324ae74 100644
--- a/src/style.css
+++ b/src/style.css
@@ -27,6 +27,8 @@ canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block
 .views button.active { background: #1d2942; color: var(--accent); }
 #spin { left: auto; right: 12px; bottom: auto; top: 12px; }
 .labels3d { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
+/* Show labels, off: the 3D views' captions go; the map stops drawing its names (map.ts). */
+#app.no-labels .labels3d { display: none; }
 .labels3d span {
   position: absolute; transform: translate(-50%, -50%); white-space: nowrap;
   font: 11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
diff --git a/src/ui/app.ts b/src/ui/app.ts
index 0a44625..c247041 100644
--- a/src/ui/app.ts
+++ b/src/ui/app.ts
@@ -23,6 +23,8 @@ type ViewName = 'map' | 'exterior' | 'interior'
 const VIEW_KEY = 'tower.view'
 /** The exterior's hull reading, remembered across reloads like the view itself (D-061). */
 const HULL_KEY = 'tower.exteriorHull'
+/** Whether labels are shown, in every view, remembered the same way. */
+const LABELS_KEY = 'tower.labels'
 const isView = (v: string | null): v is ViewName =>
   v === 'map' || v === 'exterior' || v === 'interior'
 
@@ -560,6 +562,11 @@ export class App {
     }
     // F frames the map, but not while flying: it sits beside W A S D.
     if (e.key === 'f' && this.view === 'map') this.map.fit()
+    if (e.key === 'l' || e.key === 'L') {
+      const box = $<HTMLInputElement>('f-labels')
+      box.checked = !box.checked
+      box.dispatchEvent(new Event('change'))
+    }
   }
 
   // ---------------------------------------------------------------------- view
@@ -583,6 +590,24 @@ export class App {
         localStorage.setItem(HULL_KEY, ext.value)
       } catch { /* not remembering the hull is survivable */ }
     }
+
+    // One switch for every view's labels: the map's names, and the 3D views' captions.
+    const labels = $<HTMLInputElement>('f-labels')
+    try {
+      if (localStorage.getItem(LABELS_KEY) === 'off') labels.checked = false
+    } catch { /* unreadable storage just means labels on */ }
+    const applyLabels = () => {
+      $('app').classList.toggle('no-labels', !labels.checked)
+      this.map.showLabels = labels.checked
+      this.map.draw()
+    }
+    labels.onchange = () => {
+      applyLabels()
+      try {
+        localStorage.setItem(LABELS_KEY, labels.checked ? 'on' : 'off')
+      } catch { /* not remembering it is survivable */ }
+    }
+    applyLabels()
   }
 
   // ------------------------------------------------------------------ readouts
diff --git a/src/views/map.ts b/src/views/map.ts
index 51f9f3c..7a1b9ce 100644
--- a/src/views/map.ts
+++ b/src/views/map.ts
@@ -40,6 +40,8 @@ export class TowerMap {
   /** The generated terrain, drawn under everything. Null until the first world is drawn. */
   private terrain: CanvasImageSource | null = null
   showTerrain = true
+  /** Place names and the caps' captions. The scale bar stays: it is a measure, not a label. */
+  showLabels = true
 
   private panning: { x: number; y: number; ox: number; oy: number } | null = null
   private moved = false
@@ -327,7 +329,9 @@ export class TowerMap {
       }
     }
 
-    if (slatCount() === 0) {
+    if (!this.showLabels) {
+      // Nothing to caption.
+    } else if (slatCount() === 0) {
       // The single strip is centred on the map edge, so label it in the middle
       // where it can actually be read.
       ctx.fillText('NO SLATS — ONE UNBROKEN SURFACE', x0 + 6, y0 + h / 2)
@@ -386,6 +390,7 @@ export class TowerMap {
     ctx.lineTo(farX, y0 + h)
     ctx.stroke()
 
+    if (!this.showLabels) return
     ctx.font = '11px ui-monospace, SFMono-Regular, Menlo, monospace'
     ctx.fillStyle = 'rgba(255,214,140,0.85)'
     ctx.fillText('SUN CAP', sunX + 8, y0 + 14)
@@ -538,6 +543,7 @@ export class TowerMap {
   }
 
   private label(text: string, x: number, y: number, color: string, centred: boolean): void {
+    if (!this.showLabels) return
     const ctx = this.ctx
     const w = ctx.measureText(text).width
     const box = { x: centred ? x - w / 2 : x, y: y - 11, w, h: 14 }