SONG MAP LAB

D3 · DISPLAY EVOLUTION · 1.1+ ARC · 2026-08-19
Today the pill track asks opacity to answer “where am I” — and inside one section colour opacity has 1.33:1 of headroom to say it with. The evolved display answers that question with position and language instead, and lets brightness do the one job it is good at: spent vs. not-spent. Three bands, three viewing distances, one data model — NOW (read at 2 m), NEXT (caught in the corner of the eye), FORM (read on a deliberate look).
TEMPO 120
SQUINT halves the render to approximate doubling the viewing distance — an approximation only (a CSS pixel is not a device point on your monitor). GRAYSCALE proves nothing here is hue-only.
A · TODAY, RECREATEDSongMapPillTrack.swift at true point geometry — 20pt pill, 8pt labels, 4pt gap
METRONOME
PERFORMANCE
120
BPM
WHAT THE STRIP CANNOT TELL YOU
· which section — only a 3-letter tag at 8pt (≈3 arcmin at 1 m)
· how deep into it — there is no bar-of-section count anywhere
· what is coming — bar +1 renders identically to bar +40
· the shape of the tune — 24 bars, ~9 visible, the rest scrolled away
B · EVOLVED · PERFORMANCEthree bands: section plate / approach gutter / form ribbon
METRONOME
PERFORMANCE
120
BPM
INTRO
1/2
FRAME
BARS
NEXT ▸
VERSE
IN
2
BAR 1 / 24  
C · EVOLVED · EDITsame tree, edit density — you edit 7 sections, not 24 bars
METRONOME
EDIT
120
BPM
CHORUS
6
PAYOFF
BARS IN SECTION
+  INSERT SECTION AFTER
SELECTED SECTION · CHORUS ALL-BARS GRID
Tap a ribbon block to select it. The per-bar grid below shows only that section’s bars — 58 pt tiles, above the 44 pt touch floor the shipped 26 pt pill misses.

THREE BANDS · THREE DISTANCES

EACH SCALE OWNED EXACTLY ONCE
BAND 1SECTION PLATE
What am I playing, how deep in. Section name 46 px Bebas (cap 32.2 pt ≈ 17.2 arcmin @1 m — above R1’s 16-arcmin floor, below the 20–22 band the BPM numeral owns). Bar-of-section counter 40 px Bebas. Hairline filament fills across the section.
BAND 2APPROACH GUTTER
What is coming, and when. Two-stage telegraph. Dim “NEXT ▸ X” always; the numeral and the ramp only inside the last two bars. Luminance + motion, never hue (R1 §5.2 — abrupt colour change alone does not capture attention).
BAND 3FORM RIBBON
Where am I in the whole tune. Fits to width — never scrolls, so the shape of the song is always visible. Names engraved once per section block, not once per bar. Deliberate-look tier: 8.5 px labels are not claimed to be glanceable.
SCALE LAWone scale, one element
beat → the dot lane (already LIT).  bar → the ribbon playhead steps.  section → the plate filament fills.
Nothing renders the same scale twice, so nothing competes for the glance.
ONE-GLOW LAW HELD. LIT #F0A040 appears in exactly two places on this screen: the beat dot, and the BPM numeral at the instant a Song Map tempo actually changes (the readout is alive). The playhead is ink #EAD8B0 and earns its salience by moving — R1 §5.2 ranks motion as an independent peripheral channel, so it does not need to be the brightest thing to be the most noticeable. Anticipation is never LIT — same call the July warm-up mockup made.

THE TELEGRAPH · TWO STAGES

EXTENDS THE BANKED .APPROACHING CURVE — DOES NOT REPLACE IT
BAR n−3
n−2
n−1
n
CHORUS
        quiet —————————— STAGE 1 text · STAGE 2 ramp · catch
STAGE 12 bars out
The gutter numeral lights and counts bars: IN 2IN 1. A text channel — no new glow, no second light source. At 120 BPM 4/4 that is 4 seconds of warning; the July mockup’s single-bar ramp gave 2, which is a rhythm-game budget, not a musician’s — you cannot set up a fill in two seconds.
STAGE 2final bar
The numeral drops to beat resolution — 4 · 3 · 2 · 1 — while the banked curve runs verbatim underneath it: opacity = 0.70 + 0.24·t², t = (currentBeat+1)/beatsPerBar, beat-stepped, capped at 0.94, riding the gutter filament and the ribbon’s next block. Two channels agreeing is motion-principle 1; it is not a second light source.
THE FIXre-based, not re-curved
With .future = 0.70 the curve’s beat-1 step is 1.02:1 — invisible. Re-basing .future → 0.45 makes the same curve open at 1.43:1 and close at 1.80:1. The curve was right; its floor was too high.
RESTRAINTunchanged
Ordinary bar→bar transitions inside a section get nothing. Triggers stay exactly as banked: section start, or isTempoChange(at:) (SongMapPillTrack.swift:76-80). Loop wrap adds one: ↺ LOOP IN 1.

STATE CHANNELS · MEASURED

WCAG RELATIVE LUMINANCE · EMBER GROUND #0C0C0E (Y=0.0037)
claimtodayproposed
spread across the 23 section colours 32.9×
Y 0.0145 → 0.4777
5 roles
hue-separated
a spent verse vs. the bar you are playing (outro) 2.04× brighter
0.1287 vs 0.0630
impossible
past = one ash token
.future → .playing step inside one hue 1.33:1 n/a — NOW is
position, not brightness
what carries “this bar is now” opacity + ring + ×1.06 scale + shadow
(four decorations doing one job)
the playhead sits on it
+ the plate names it
tap target, narrowest pill 26 pt wide
(minWidth :169) vs 44 pt HIG floor
ribbon is display-only;
editing is 58 pt tiles
WHY 23 COLOURS CANNOT WORK. Hold every section at one luminance so that brightness is free to mean state, and 20 distinct hues must share 360° — the computed worst neighbours land 0.3° apart (verse/one-drop) and 1.0° apart (chorus/trading-4s). Let luminance vary to separate them and it swamps the state channel again, which is exactly what ships today. The constraint is geometric, not aesthetic. So colour stops being the section’s identity and becomes its ROLE; the NAME is the identity, at 46 px.

THE ROLE RAMP · 5, NOT 23

PROPOSAL — REQUIRES OWNER RULING; TOUCHES SectionType.color SEMANTICS
rolesectionsY / CR
FRAME intro, outro, out head, rest0.115 / 3.07:1
STATEMENT verse, head, theme, exposition, recap, one drop, riddim0.315 / 6.79:1
LIFT pre-chorus, development, variation0.560 / 11.35:1
PAYOFF chorus, hook, shout chorus0.330 / 7.07:1
DEPARTURE bridge, solo, trading 4s, break, dub0.205 / 4.75:1
ASH every spent bar, whatever its role0.033 / 1.54:1
Separation, CIE76 ΔE across the five: normal 33.5 · protanopia 22.9 · deuteranopia 16.8 · tritanopia 5.6 (LIFT/PAYOFF — the one weak pair; they differ by ΔL* 15.4 so grayscale still splits them, and the block name is always engraved beside them). ASH clears the F6 visibility floor at 1.54:1 (Theme.swift:120-121) and sits ΔE 20.7 from the nearest role. The gold→hot-orange LIFT→PAYOFF pair is the ember DNA doing the semantic work: the tune literally heats up into the chorus.

DENSITY LADDER · THE RIBBON UNDER STRESS

FIT-TO-WIDTH DEGRADES; A SCROLLER JUST CLIPS
Rules, in order: below 6 px per bar the per-bar rules are dropped and only section blocks survive; below 34 px per block the engraved name elides to its initial; below 14 px the name is dropped entirely and the block is colour-only. Nothing is ever clipped off-screen — the whole form is always present, which is the property a horizontally scrolling strip structurally cannot have.

UNMAPPED IS NOT GREY MUSH. A fresh map is 8 bars of .empty (SongMapModel.swift:23) and renders today as eight identical blank chiclets — a poor first impression of the flagship practice surface. Unmapped renders instead as an honest amber bar ruler: numbered, playable, obviously un-authored.
Every colour literal is shipped code — Ember Theme.swift:122-146, Midnight :246-270, legacy section hues SongMapSection.swift:110-136. Panel A geometry is SongMapPillTrack.swift:85, 153, 163, 169-170, 184, 204-238.
Type sizes are annotated with cap-height and arc-minute subtense per R1-glanceability.md (163 pt/in, Bebas cap ratio 0.70; the Overpass Mono ratio is still unverified — R1 §3). The luminance arithmetic is WCAG relative luminance computed on the shipped hexes; the alpha compositing is done in linear light and is approximate for the on-device blend.