diff options
68 files changed, 3100 insertions, 1 deletions
diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..fd4eb2ad --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Unified diff context lines can carry a required trailing space marker. +working/**/*.patch whitespace=-blank-at-eol @@ -110,4 +110,6 @@ todo.org # Claude Code: task archive (follows todo file privacy) /archive/task-archive.org takuzu-stats.eld -/working/ + +# disposable project-local scratch; active task artifacts live in versioned working/ +/temp/ diff --git a/working/eat-sixel-patch/eat-xtwinops.patch b/working/eat-sixel-patch/eat-xtwinops.patch new file mode 100644 index 00000000..4347d729 --- /dev/null +++ b/working/eat-sixel-patch/eat-xtwinops.patch @@ -0,0 +1,46 @@ +--- a/eat.el ++++ b/eat.el +@@ -2809,6 +2809,33 @@ + (funcall (eat--t-term-input-fn eat--t-term) eat--t-term + "\e[>0;0;0c"))))) + ++(defun eat--t-send-window-size-report (n) ++ "Respond to XTWINOPS window size report request N. ++ ++N is 14 (text area size in pixels), 16 (cell size in pixels) or 18 ++\(text area size in characters). Other operations are ignored. ++Multiplexers like tmux send \\='CSI 14 t\\=' to discover the cell ++pixel size they need before they will emit Sixel to a terminal." ++ (let ((disp (eat--t-term-display eat--t-term))) ++ (pcase n ++ (14 ++ (funcall (eat--t-term-input-fn eat--t-term) eat--t-term ++ (format "\e[4;%i;%it" ++ (* (eat--t-disp-height disp) ++ (eat--t-term-char-height eat--t-term)) ++ (* (eat--t-disp-width disp) ++ (eat--t-term-char-width eat--t-term))))) ++ (16 ++ (funcall (eat--t-term-input-fn eat--t-term) eat--t-term ++ (format "\e[6;%i;%it" ++ (eat--t-term-char-height eat--t-term) ++ (eat--t-term-char-width eat--t-term)))) ++ (18 ++ (funcall (eat--t-term-input-fn eat--t-term) eat--t-term ++ (format "\e[8;%i;%it" ++ (eat--t-disp-height disp) ++ (eat--t-disp-width disp))))))) ++ + (defun eat--t-send-graphics-attrs (attr operation) + "Send graphics attributes. + +@@ -3632,6 +3659,9 @@ + ;; CSI s. + (`((?s) nil nil) + (eat--t-save-cur)) ++ ;; CSI <n> t. ++ (`((?t) nil ((,n))) ++ (eat--t-send-window-size-report n)) + ;; CSI u. + (`((?u) nil nil) + (eat--t-restore-cur))))))) diff --git a/working/eat-sixel-patch/rulesets-companion-note.org b/working/eat-sixel-patch/rulesets-companion-note.org new file mode 100644 index 00000000..2df33273 --- /dev/null +++ b/working/eat-sixel-patch/rulesets-companion-note.org @@ -0,0 +1,5 @@ +#+TITLE: EAT XTWINOPS patch companion note +#+SOURCE: from rulesets +#+DATE: 2026-07-13 10:58:54 -0500 + +EAT XTWINOPS patch (companion file eat-xtwinops.patch, sent separately just now): verified live on ratio 2026-07-13, this makes sixel images render AND persist in tmux inside EAT. Root cause chain: tmux 3.7b has native sixel but refuses to transmit unless it knows the client's cell pixel size; it asks via CSI 14 t (XTWINOPS), which EAT 0.9.4 silently ignores (its CSI dispatch has no 't' case). The patch adds eat--t-send-window-size-report answering CSI 14/16/18 t from eat--t-term-char-width/height and the display dims — same fields EAT's own XTSMGRAPHICS reply already uses. With it, tmux learns the geometry at client attach, ingests raw sixel into its grid, and re-emits on every redraw — images survive window switches, scrolling, and resizing (Craig verified visually). Needs on the emacsd side: (1) a durable home for the patch — the elpa file is version-pinned, so likely a load-path shadow or an advice-free vendored fix, your call, plus wiring so both daily drivers load it (velox verified ready: same tmux build, imagemagick present); (2) written test-first per testing.md when productionizing — this was a timeboxed spike, treat the diff as evidence not deliverable; (3) strongly consider sending it upstream to EAT (codeberg akib/emacs-eat) so 0.9.5+ ships it stock. tmux side is separate (two conf lines, handoff sent to dotfiles). Ask rulesets for the full investigation log if useful. diff --git a/working/eat-sixel-patch/takuzu-original-bug-report.org b/working/eat-sixel-patch/takuzu-original-bug-report.org new file mode 100644 index 00000000..02107a88 --- /dev/null +++ b/working/eat-sixel-patch/takuzu-original-bug-report.org @@ -0,0 +1,17 @@ +#+TITLE: Bug/investigate: images from Claude Code don't display in th +#+SOURCE: from takuzu +#+DATE: 2026-07-11 14:41:39 -0500 + +Bug/investigate: images from Claude Code don't display in the EAT terminal. + +Context: running Claude Code inside EAT (eat 0.9.4 from elpa). When the agent posts an image to the terminal, nothing renders — Craig can't see it. + +Finding: EAT *does* have Sixel image support. eat.el defines eat-sixel-scale, eat-sixel-aspect-ratio, eat-sixel-render-formats, and a full Sixel decode/render path (sixel-buffer, sixel-palette, sixel-render-format, etc.). So the capability exists; this isn't a plain 'EAT can't do images' limitation. + +Likely causes to check: +1. Protocol mismatch: Claude Code CLI probably emits the kitty graphics protocol or the iTerm2 inline-image protocol (OSC 1337), NOT Sixel. EAT renders Sixel only, so a kitty/iTerm image sequence is dropped. Confirm which protocol Claude Code uses (env like TERM, or its terminal-capability detection) and whether it can be told to emit Sixel. +2. Sixel render-format availability: eat-sixel-render-formats lists preferred render backends; if none is available in this Emacs build, Sixel silently won't display. Test EAT with a known-good sixel image (e.g. img2sixel output) to confirm EAT-side rendering works at all. + +Repro: in an EAT session, have Claude Code (or any tool) post an image; observe nothing renders. Next step: identify the emitted escape sequence (capture raw bytes) to confirm protocol. + +From the takuzu session, 2026-07-11 — surfaced because the agent kept posting game screenshots the terminal couldn't show. diff --git a/working/music-svg-app-design/2026-07-19-music-config-ui-remodel-render-fixture.svg b/working/music-svg-app-design/2026-07-19-music-config-ui-remodel-render-fixture.svg new file mode 100644 index 00000000..aad6f746 --- /dev/null +++ b/working/music-svg-app-design/2026-07-19-music-config-ui-remodel-render-fixture.svg @@ -0,0 +1,87 @@ +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + viewBox="0 0 1916 821" width="1916" height="821"> + <defs> + <radialGradient id="glassGlow" cx="40%" cy="0" r="90%"> + <stop offset="0" stop-color="#22231f" stop-opacity=".28"/> + <stop offset="1" stop-color="#050605" stop-opacity="0"/> + </radialGradient> + <linearGradient id="headerBrass" x1="0" x2="1"> + <stop offset="0" stop-color="#c4aa89"/> + <stop offset=".5" stop-color="#ae9374"/> + <stop offset="1" stop-color="#c2a27d"/> + </linearGradient> + <clipPath id="metadataClip"><rect x="780" y="104" width="350" height="180"/></clipPath> + <clipPath id="playlistClip"><rect x="1188" y="126" width="610" height="520"/></clipPath> + </defs> + + <image x="0" y="0" width="1916" height="821" + xlink:href="concepts/30a-dupre-studios-user-refined-playlist.png"/> + + <rect x="605" y="96" width="540" height="250" fill="#070807"/> + <rect x="605" y="96" width="540" height="250" fill="url(#glassGlow)"/> + <image x="612" y="108" width="146" height="173" xlink:href="assets/vinyl-placeholder.svg"/> + <g clip-path="url(#metadataClip)" fill="#ebe5d9" font-family="Berkeley Mono, monospace" font-size="20" letter-spacing="1.4"> + <text x="780" y="130">A CHICKEN WITH ITS HEAD CUT OFF — REMASTERED ANNIVERSARY EDITION</text> + <text x="780" y="166">THE MAGNETIC FIELDS AND THE LONG-LOST ORCHESTRA</text> + <text x="780" y="202">69 LOVE SONGS: THE COMPLETE THREE-VOLUME COLLECTION</text> + <text x="780" y="238">MERGE RECORDS</text> + <text x="780" y="274">1999</text> + </g> + <text x="610" y="322" fill="#ddd4c5" font-family="Berkeley Mono, monospace" font-size="19">01:30</text> + <line x1="679" y1="316" x2="1023" y2="316" stroke="#3b3a37" stroke-width="2"/> + <line x1="679" y1="316" x2="850" y2="316" stroke="#f2a900" stroke-width="2"/> + <circle cx="850" cy="316" r="9" fill="#c79a60" stroke="#f0d4a7" stroke-width="1"/> + <text x="1040" y="322" fill="#ddd4c5" font-family="Berkeley Mono, monospace" font-size="19">02:59</text> + + <rect x="1172" y="52" width="688" height="65" fill="url(#headerBrass)"/> + <g fill="#251c14" font-family="Berkeley Mono, monospace" font-size="20" letter-spacing="1.5"> + <text x="1202" y="94">PLAYLIST</text> + <text x="1316" y="94">|</text> + <text x="1355" y="94">69 LOVE SONGS — COMPLETE</text> + <text x="1731" y="94">69 TRACKS</text> + </g> + <rect x="1172" y="118" width="688" height="545" fill="#070807"/> + <rect x="1172" y="118" width="688" height="545" fill="url(#glassGlow)"/> + <g clip-path="url(#playlistClip)" font-family="Berkeley Mono, monospace" font-size="17" letter-spacing=".8"> + <g fill="#ddd8cf"> + <text x="1192" y="153">01</text><text x="1237" y="153">ABSOLUTELY CUCKOO — THE MAGNETIC FIELDS</text><text x="1760" y="153">02:02</text> + <text x="1192" y="190">02</text><text x="1237" y="190">I DON’T BELIEVE IN THE SUN — THE MAGNETIC FIELDS</text><text x="1760" y="190">02:50</text> + <text x="1192" y="227">03</text><text x="1237" y="227">ALL MY LITTLE WORDS — THE MAGNETIC FIELDS</text><text x="1760" y="227">02:03</text> + </g> + <rect x="1175" y="238" width="650" height="37" fill="#f2a900" fill-opacity=".07"/> + <g fill="#f2a900"> + <text x="1174" y="264">▶</text><text x="1192" y="264">04</text><text x="1237" y="264">A CHICKEN WITH ITS HEAD CUT OFF — THE MAGNETIC FIELDS</text><text x="1760" y="264">02:59</text> + </g> + <g fill="#ddd8cf"> + <text x="1192" y="301">05</text><text x="1237" y="301">(I’M) DAZOTA — THE MAGNETIC FIELDS</text><text x="1760" y="301">02:38</text> + <text x="1192" y="338">06</text><text x="1237" y="338">I DON’T WANT TO GET OVER YOU — THE MAGNETIC FIELDS</text><text x="1760" y="338">02:50</text> + <text x="1192" y="375">07</text><text x="1237" y="375">COME BACK FROM SAN FRANCISCO — THE MAGNETIC FIELDS</text><text x="1760" y="375">02:37</text> + <text x="1192" y="412">08</text><text x="1237" y="412">THE LUCKIEST GUY ON THE LOWER EAST SIDE — THE MAGNETIC FIELDS</text><text x="1760" y="412">01:32</text> + <text x="1192" y="449">09</text><text x="1237" y="449">LET’S PRETEND WE’RE BUNNY RABBITS — THE MAGNETIC FIELDS</text><text x="1760" y="449">02:10</text> + <text x="1192" y="486">10</text><text x="1237" y="486">THE CACTUS WHERE YOUR HEART SHOULD BE — THE MAGNETIC FIELDS</text><text x="1760" y="486">02:38</text> + <text x="1192" y="523">11</text><text x="1237" y="523">I THINK I NEED A NEW HEART — THE MAGNETIC FIELDS</text><text x="1760" y="523">02:58</text> + <text x="1192" y="560">12</text><text x="1237" y="560">THE BOOK OF LOVE — THE MAGNETIC FIELDS</text><text x="1760" y="560">02:56</text> + <text x="1192" y="597">13</text><text x="1237" y="597">FIDO, YOUR LEASH IS TOO LONG — THE MAGNETIC FIELDS</text><text x="1760" y="597">02:13</text> + <text x="1192" y="634">14</text><text x="1237" y="634">HOW FUCKING ROMANTIC — THE MAGNETIC FIELDS</text><text x="1760" y="634">00:58</text> + </g> + </g> + + <g fill="#171714" stroke="#6b5a3c" stroke-width="1.5"> + <circle cx="862" cy="614" r="6"/><circle cx="850" cy="588" r="6"/><circle cx="846" cy="559" r="6"/> + <circle cx="850" cy="530" r="6"/><circle cx="861" cy="502" r="6"/><circle cx="878" cy="479" r="6"/> + <circle cx="901" cy="461" r="6"/><circle cx="928" cy="451" r="6"/><circle cx="957" cy="447" r="6"/> + <circle cx="986" cy="451" r="6"/><circle cx="1013" cy="461" r="6"/><circle cx="1036" cy="479" r="6"/> + <circle cx="1053" cy="502" r="6"/><circle cx="1064" cy="530" r="6"/><circle cx="1068" cy="559" r="6"/> + <circle cx="1064" cy="588" r="6"/><circle cx="1052" cy="614" r="6"/> + </g> + <g fill="#ffb23c" stroke="#fff0bd" stroke-width="1.5"> + <circle cx="862" cy="614" r="6"/><circle cx="850" cy="588" r="6"/><circle cx="846" cy="559" r="6"/> + <circle cx="850" cy="530" r="6"/><circle cx="861" cy="502" r="6"/><circle cx="878" cy="479" r="6"/> + <circle cx="901" cy="461" r="6"/><circle cx="928" cy="451" r="6"/><circle cx="957" cy="447" r="6"/> + <circle cx="986" cy="451" r="6"/><circle cx="1013" cy="461" r="6"/> + </g> + + <line x1="227" y1="537" x2="245" y2="467" stroke="#f3d59c" stroke-width="2"/> + <line x1="480" y1="537" x2="496" y2="466" stroke="#f3d59c" stroke-width="2"/> +</svg> diff --git a/working/music-svg-app-design/2026-07-19-music-config-ui-remodel-spec.org b/working/music-svg-app-design/2026-07-19-music-config-ui-remodel-spec.org new file mode 100644 index 00000000..6e68e104 --- /dev/null +++ b/working/music-svg-app-design/2026-07-19-music-config-ui-remodel-spec.org @@ -0,0 +1,923 @@ +#+TITLE: Music Config UI Remodel — Spec +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-19 +#+TODO: TODO | DONE +#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED + +* DRAFT Music Config UI Remodel +:PROPERTIES: +:ID: bcbb9342-1884-4281-8821-89b06f6f8793 +:END: +- 2026-07-19 Sun @ 16:03:13 -0500 — drafted from the approved Dupre Studios design session. + +** Prototype iterations + +The design session started with working HTML directions, moved through six +distinct visual concepts, and then refined one receiver layout through paired +playlist/radio boards. The last board is Craig's GIMP refinement and is the +visual authority for this spec. + +- [[file:music-svg-directions.html][Initial working SVG directions]] +- [[file:concept-board.html][Six-direction comparison board]] +- [[file:concepts/01-champagne-receiver.png][01 Champagne Receiver]] +- [[file:concepts/02-le-mans-night-cluster.png][02 Le Mans Night Cluster]] +- [[file:concepts/03-geneva-playback-chronograph.png][03 Geneva Playback Chronograph]] +- [[file:concepts/04-mastering-room-reel-console.png][04 Mastering Room Reel Console]] +- [[file:concepts/05-perpetual-calendar-salon.png][05 Perpetual Calendar Salon]] +- [[file:concepts/06-transatlantic-broadcast-navigator.png][06 Transatlantic Broadcast Navigator]] +- [[file:concepts/07-functional-black-glass-receiver.png][07 Functional black-glass receiver]] +- [[file:concepts/08-functional-black-glass-flipped.png][08 Playlist-flipped receiver]] +- [[file:concepts/09-functional-black-glass-retro.png][09 Warmer retro receiver]] +- [[file:concepts/10-warm-black-glass-player-volume.png][10 Player-volume receiver]] +- [[file:concepts/11-illuminated-black-glass-controls.png][11 Illuminated controls]] +- [[file:concepts/12-consolidated-functional-receiver.png][12 Consolidated functional receiver]] +- [[file:concepts/13-black-silver-chronograph-receiver.png][13 Black-silver chronograph]] +- [[file:concepts/14-corrected-luxury-chronograph-receiver.png][14 Corrected luxury chronograph]] +- [[file:concepts/15-champagne-brass-digital-hifi.png][15 Champagne digital hi-fi]] +- [[file:concepts/16-champagne-aluminum-radio-groups.png][16 Explicit radio grouping]] +- [[file:concepts/17-dupre-coltrane-scrollbar.png][17 Long-playlist and scrollbar stress test]] +- [[file:concepts/18-dupre-branding-control-study.png][18 Branding and control study]] +- [[file:concepts/19a-dupre-playlist-state.png][19a Playlist state]] and [[file:concepts/19b-dupre-radio-state.png][19b radio state]] +- [[file:concepts/20-dupre-playlist-radio-comparison.png][20 Long playlist / compact radio comparison]] +- [[file:concepts/21-dupre-semantic-controls-comparison.png][21 Semantic control comparison]] +- [[file:concepts/22-dupre-identity-bay-comparison.png][22 Identity-bay comparison]] +- [[file:concepts/23-dupre-swapped-info-comparison.png][23 Swapped information comparison]] +- [[file:concepts/24-dupre-full-brass-vu-comparison.png][24 Full-brass VU comparison]] +- [[file:concepts/25-dupre-seamless-flush-comparison.png][25 Seamless flush comparison]] +- [[file:concepts/26-dupre-studios-rounded-chrome-comparison.png][26 Dupre Studios comparison]] +- [[file:concepts/27-dupre-studios-leather-analog-header-comparison.png][27 Leather and analog-header comparison]] +- [[file:concepts/28-dupre-studios-balanced-controls-comparison.png][28 Balanced-control comparison]] +- [[file:concepts/29a-dupre-studios-brass-seek-playlist.png][29 Brass seek-thumb pass]] +- [[file:concepts/30a-dupre-studios-user-refined-playlist.png][30 Craig's near-final playlist board (visual authority)]] +- [[file:prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html][31 Functional local-playlist / radio prototype (acceptance candidate)]] + +* Metadata +| Status | Not ready — first review found one functional-prototype blocker | +|----------+------------------------------------------------------------------------| +| Owner | Craig Jennings | +|----------+------------------------------------------------------------------------| +| Reviewer | Craig Jennings | +|----------+------------------------------------------------------------------------| +| Related | [[file:../../todo.org][music: SVG player application design and delivery]]; [[file:../../docs/specs/2026-07-06-fancy-music-player-ui-spec.org][implemented fancy player spec]]; [[file:../../docs/specs/music-config-without-emms-spec.org][EMMS-free architecture spec]] | +|----------+------------------------------------------------------------------------| + +* Summary + +Replace the current text-and-image EMMS playlist surface with a realistic +Dupre Studios receiver rendered inside Emacs. The interface uses a raster skin +for leather, glass, brass, and machined controls, then draws all changing +information and illumination in SVG. Emacs Lisp remains the application +runtime and maps the SVG surface to the existing music-config commands. + +The remodel preserves the plain and fancy text views as fallbacks. It does not +change M3U ownership, radio-browser behavior, or the current EMMS playback model. + +* Problem / Context + +The implemented fancy player improved names, artwork, and progress, but it is +still a text playlist with a multi-line overlay. It cannot present the player +as one coherent instrument. Controls are key hints rather than controls, +playlist management is visually detached from playback, and active state is +limited to text faces and a character-cell progress bar. + +Craig wants a bottom-docked application that looks like a manufactured object: +black leather, black glass, champagne brass, lit analog meters, and discreet +controls. The beauty has to explain state. A lamp, depressed key, moving +needle, seek position, or selected row must correspond to real music-config or +mpv behavior. Decorative AM/FM, tone, power, reload, manual URL, and other +unsupported faceplate controls are prohibited. + +The final 1916×821 board settles the visual direction. The remaining design +problem is how to preserve its realism without baking live state into a picture. +An all-vector redraw would lose much of the leather and metal detail. A single +clickable PNG would preserve the appearance but freeze track data, meter state, +playlist rows, and button feedback. The chosen design separates the physical +object from its information and behavior. + +* Goals and Non-Goals + +** Goals + +- Match Craig's near-final board closely enough that the running player reads as + the same manufactured receiver, not a simplified diagram of it. +- Preserve the full music-config workflow: playback, seek, player volume, + playlist browsing and persistence, radio search, repeat/single/random/consume, + reordering, add, new, load, save, and delete. +- Make every visible state honest. Meter needles use real program levels, + volume lamps use mpv volume, progress uses mpv position, and mode lamps use + current mode state. +- Keep the hot path compact: album information and controls on the left, + fourteen playlist rows and playlist actions on the right. +- Keep keyboard operation complete while adding direct mouse interaction. +- Preserve the existing text render for TTY frames, unsupported SVG builds, and + opt-out/rollback. +- Keep the renderer independent of EMMS objects so the planned EMMS-free/VAMP + work can replace the controller without replacing the visual layer. + +** Non-Goals + +- No music library browser, tag editor, lyrics view, waveform editor, or album + collection database. +- No streaming-service integration. +- No fake receiver functions such as AM/FM tuning, bass, treble, speakers, or + power. +- No permanent faceplate controls for reload, editing the M3U file, or manual URL + entry. Existing keyboard and M-x access may remain. +- No all-vector imitation of leather, brushed metal, or photographic reflections. +- No EMMS removal or VAMP extraction in this project. Those remain separate + architecture efforts. +- No alternate narrow/mobile layout in v1. The player scales as one panoramic + instrument. + +** Scope tiers + +- v1: neutral raster skin, SVG renderer, dedicated graphical view buffer, + complete keyboard/mouse command surface, playlist viewport, seek, persistent + mpv volume, real stereo VU telemetry, local-file and radio states, diagnostics, + fallbacks, tests, and live verification. +- Out of scope: library browsing, ratings, per-track menus, editable metadata, + alternate skins, responsive reflow, and remote control. +- vNext: multiple visual skins, a narrower alternate layout, richer motion + transitions, and reuse in a future standalone VAMP package. + +* Design + +** User view + +F10 opens the player in the existing bottom side-window position. In a +graphical frame with the remodel enabled, the buffer is the Dupre Studios +receiver. The same keys still work. The visible buttons also work with the +mouse, the playlist scrolls inside its fourteen-row aperture, and selecting a +row starts that track. + +A local file shows album art, title, artist, album, label, and year on separate +lines when those fields exist. It shows elapsed and total time with a draggable +seek control. A live station shows station art and two ON AIR indications: one +under the art and one in the active station row. It never shows a seek control +or invented time for a stream. Missing metadata produces no placeholder word. + +The interface behaves like hardware. Play/Pause sits depressed and glows green +while playing. Momentary transport controls brighten while pressed. Active +play modes illuminate the circumference of their brass buttons. Volume lamps +fill continuously to the current mpv volume. VU needles move from the actual +left and right audio levels. + +The interaction model is available in the [[file:prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html][functional browser prototype]]. It +switches between a 69-track local fixture and an 18-station radio fixture and +wires transport, playback modes, radio search, playlist actions, row selection, +seek, volume, wheel/keyboard browsing, scrollbar drag, empty state, and delete +confirmation. Its built-in twelve-check behavior pass is green. Craig's +hands-on acceptance remains the last prototype gate. + +** Research and visual sources + +The design session compared NAD restraint, Marshall's black-and-brass control +language, McIntosh black glass and meter hierarchy, vintage stereo transport +controls, automobile instrument clusters, and black/silver chronographs. The +useful lessons were consistent: controls need one visual family, status lights +need stable color meaning, labels belong on the faceplate, and ornament should +make state easier to read. + +The local Dupre references are the implementation sources: + +- [[file:references/dupre-gallery.png][Dupre component gallery capture]] +- [[file:references/waybar-redesign.png][Waybar redesign capture]] +- [[file:README.org][Design-session decisions and critique log]] +- [[file:concepts/30a-dupre-studios-user-refined-playlist.png][Near-final visual authority]] +- [[file:../../../code/archsetup/docs/prototypes/gallery-widget.el][svg.el Dupre renderer proof of concept]] + +** Canonical geometry + +The source canvas is 1916×821 with a viewBox of =0 0 1916 821=. The renderer +preserves that aspect ratio and scales it to the available window width. It +does not reflow the playlist under the controls or move controls at runtime. +Extra vertical room stays black below the instrument. A window too short for +legible rendering falls back to the existing fancy text view rather than +shrinking controls into unusable targets. + +Craig's final board defines the visual positions. Phase 1 records them in one +geometry table, including every visual rectangle, label anchor, hit target, and +playlist row. Rendering and hit testing use the same constants. No second +hand-maintained map is allowed. + +The principal regions are: + +| Region | Approximate source bounds | +|------------------------+---------------------------| +| Complete instrument | 0,0 to 1916,821 | +| Player upper glass | 50,50 to 1170,368 | +| Lower brass deck | 69,372 to 1147,744 | +| Playlist glass | 1170,52 to 1862,744 | +| Branding field | 150,115 to 500,275 | +| Artwork | 609,108 to 755,281 | +| Metadata | 780,100 to 1125,275 | +| Seek / ON AIR line | 608,302 to 1125,330 | +| Playlist viewport | 1175,126 to 1835,658 | +| Playlist actions | 1173,671 to 1850,733 | + +Exact measured values replace these orientation bounds in the geometry table. + +** Rendering layers + +The SVG is a scene graph with four layers. + +1. The physical shell is raster. It contains the leather chassis, compressed + seams, black glass texture, fine low-contrast champagne micro-brush, recessed + grooves, fixed branding, + engraved labels, neutral control bodies, neutral volume knob, and empty VU + scales. The application embeds the assets with =svg-embed= and caches their + encoded data for the Emacs session. + +2. Static vector geometry supplies clip paths, hit-region outlines used only in + debug mode, row separators, and any simple lines that need to stay sharp at + different scales. + +3. Dynamic SVG supplies album/station art, metadata, playlist header and rows, + progress, brass seek thumb, scrollbar, transport glyphs, pressed states, + lamps, VU needles, and meter light. No live text, needle, lamp, selected row, + scrollbar, or progress state is baked into the raster shell. + +4. Emacs Lisp supplies behavior. SVG rendered through librsvg does not run + browser JavaScript. A buffer-local keymap receives mouse events, + =posn-object-x-y= returns pixel coordinates inside the displayed image, and + one scale transform maps them back to the source coordinate system. + +The near-final PNG cannot be embedded unchanged. Phase 1 creates a neutral +layered source by removing metadata, playlist text, active lamps, VU needles, +progress, and selection from the final board. The source stays editable. The +editable source stays one full-resolution instrument. Runtime rendering uses +three aligned image regions: upper player glass, lower control deck, and +playlist. A compact control sprite sheet and neutral VU face supplement those +regions. The current 1916×821 art is treated as a two-times source for a typical +roughly 958×410 rendered dock. Opaque runtime crops may be exported at their +display size after a PNG/JPEG visual comparison. V1 does not upscale beyond the +native source pixels. + +** Module and state boundary + +The SVG view lives in a new =modules/music-svg-ui.el= module. It requires +=svg=, =dom=, and ordinary Emacs image support, but it does not require EMMS. +Its pure entry points accept plain data: + +- =cj/music-svg-render= takes a state snapshot and viewport. +- =cj/music-svg-hit-target= maps source coordinates to an action description. +- =cj/music-svg-truncate= fits display text to a fixed pixel/character budget. +- =cj/music-svg-scrollbar= computes thumb length and position. +- =cj/music-svg-vu-angle= maps a smoothed dB value to the meter scale. + +=modules/music-config.el= remains the controller. It builds an ephemeral +=cj/music-ui-state= snapshot from the current EMMS playlist and player state, +then dispatches renderer actions back to existing commands. The snapshot owns +no durable music data. EMMS and M3U files remain authoritative in v1. + +The snapshot contains: + +- playback status and track kind +- current track identity and metadata +- art path, elapsed time, duration, and seekability +- playlist name/file, total count, rows, selected index, and viewport offset +- repeat, single, random, and consume state +- player volume +- left/right meter values +- transient pressed/focused target + +The graphical view is a dedicated =*Music Player*= special-mode buffer. The +hidden EMMS playlist buffer continues to own playlist markers and track order. +Controller helpers translate a visible row index to an EMMS buffer position +before selection, playback, reorder, or deletion. This avoids making a single +image character pretend to be fourteen editable EMMS rows, and it leaves a +clean seam for the EMMS-free state API later. + +** Metadata and typography + +Berkeley Mono is the default instrument typeface because it is the Dupre token +font and is installed on the target system. Missing font support falls back to +=monospace=. The cursive Dupre Studios mark remains raster in the physical +shell. + +Title, artist, album, label, and year each get one line. Empty values disappear +without closing the line spacing above them. The title receives the largest +width budget and truncates with a literal ellipsis. The renderer uses the +fixed-width font metrics and the measured metadata box, not a guessed character +count from the current window. + +The playlist shows fourteen one-line rows. Each row is number, title, artist, +and duration on one line. The row text truncates before the duration column. +Inactive text is soft white. The active row is amber and carries the triangular +play marker. There are no decorative status dots. + +The header is the only coffee/cream instrument card. It shows the current +collection or search name and item count, but never labels the source as a +playlist or radio. Four concealed warm-light pools are part of the static skin, +but the text is SVG because the name and count change. Its champagne surface is +smooth metal with a fine linear micro-brush, never coarse or wavy wood grain. + +** Playlist viewport + +The UI keeps a buffer-local first-visible index. The current track is kept in +view after track changes unless the user is actively browsing elsewhere. Mouse +wheel moves three rows. Up/Down moves the UI cursor one row. Page Up/Page Down +moves fourteen. Home/End goes to the first/last track. A row click selects and +starts that track. The existing keyboard play command remains available. + +For =N= tracks and fourteen visible rows, the scrollbar thumb fraction is +=min(1, 14/N)=. Its position is =offset/(N-14)= when =N > 14=. Dragging the +thumb changes the viewport but never changes the playing track. No scrollbar +appears for fourteen or fewer tracks. + +** Control behavior + +The visible controls map to existing behavior: + +| Surface | Action | +|---------------+--------| +| PREV | =cj/music-previous= | +| PLAY/PAUSE | start selected/current track, or =emms-pause= while playing | +| STOP | =emms-stop= | +| NEXT | =cj/music-next= | +| REPEAT | =emms-toggle-repeat-playlist= | +| SINGLE | =emms-toggle-repeat-track= | +| RANDOM | =emms-toggle-random-playlist= | +| CONSUME | =cj/music-toggle-consume= | +| NAME | =cj/music-radio-search-by-name= | +| TAGS | =cj/music-radio-search-by-tag= | +| ADD | =cj/music-fuzzy-select-and-add= | +| NEW | =cj/music-playlist-clear=, including its file-association reset | +| LOAD | =cj/music-playlist-load= | +| SAVE | =cj/music-playlist-save= | +| DELETE | =cj/music-delete-playlist= with its existing strong confirmation | + +Mouse press immediately renders the physical pressed state. Release inside the +same target dispatches the command. Release elsewhere cancels it. The +Play/Pause key remains depressed while playback is active. PREV and NEXT only +brighten during a press. STOP has no latched light. + +The four play-mode controls are alternate-action pushbuttons, not lamps or +rotary controls. Each has an unmarked spun-brass cap matching the volume cap, a +fixed collar, and a narrow dark travel seam; the faceplate label identifies its +function. Off stands slightly proud and casts a small lower shadow. On sits +nearly flush, collapses that shadow, and illuminates a thin desaturated-green +ring inside the cap/collar gap. Play/Pause uses the same warm instrument green +through its cut-out symbol with a tight internal bloom; the depressed key +remains the primary playing cue. + +The SVG mode retains the full existing keyboard surface, including commands not +placed on the faceplate: manual station creation, reload, edit, append to another +playlist, shuffle, track removal, and track reordering. Which-key/help text +documents those commands. The faceplate stays limited to the approved controls. + +** Seek and volume + +Local files show elapsed time, total duration, an amber progress line, and a +narrow vertical machined-brass grip. Clicking or dragging the slider sends an +mpv =seek= command in +=absolute-percent= mode. The displayed thumb is computed from mpv =time-pos= +and =duration=. While dragging, the local preview follows the pointer; the next +mpv response confirms or corrects it. + +URL tracks always use the radio presentation. The slider and times disappear. +One red =ON AIR= line appears under the station art, and the active playlist row +contains the second =ON AIR=. No other ON AIR label is rendered. + +Player volume is separate from the system mixer. Add +=cj/music-player-volume=, default 100, and apply it to every new mpv process. +The knob has no number or pointer. Its equally spaced lamps show the stored +0–100 value. Click/drag around the arc sets volume, the mouse wheel changes it +by five, and the existing +/− bindings call the same player-volume commands. +Setting volume writes mpv's =volume= property and updates the stored value so a +track change does not reset it. + +Every volume position is one fixed lamp well. Dark and illuminated states use +the same center and mounting bezel; the lit state replaces the dark lens rather +than adding another dot or adjacent glow. The neutral raster skin contains no +baked lamp cores or light spill. + +** Real VU telemetry + +mpv starts with a labeled FFmpeg audio filter: + +#+begin_src text +--af-add=@meter:lavfi=[astats=metadata=1:reset=1:measure_perchannel=RMS_level+Peak_level:measure_overall=none] +#+end_src + +The UI reads =af-metadata/meter= over mpv JSON IPC. FFmpeg supplies +=lavfi.astats.1.RMS_level= and =lavfi.astats.2.RMS_level= (plus peak values). +Mono duplicates channel one to both meters. Missing or nonnumeric data rests +the needles and records one diagnostic; it never invents motion. + +This path was verified locally on 2026-07-19 with mpv 0.41.0 and FFmpeg 8.1.2. +A synthetic stereo source returned distinct left/right RMS and peak values +through the labeled property. The official references are: + +- [[https://mpv.io/manual/master/#command-interface-af-metadata][mpv af-metadata property]] +- [[https://ffmpeg.org/ffmpeg-filters.html#astats-1][FFmpeg astats filter]] + +The renderer maps dB to the printed meter scale (−40 through +3). A 300 ms +attack and slower 600 ms release approximate a physical VU movement. Values +below the scale rest at the left stop. Values above +3 clamp at the right stop. + +Telemetry uses one persistent, nonblocking IPC connection with request IDs and +line-buffered JSON replies. It polls only while the player view is visible and +audio is running. It closes when playback stops or the view is hidden. The +existing one-shot IPC helpers remain valid for infrequent commands. + +** Refresh and render budget + +Hooks trigger immediate redraws for track, playlist, playback, mode, and art +changes. Progress updates four times per second while a seekable file plays. +Meters target ten updates per second while visible and playing. Paused, stopped, +hidden, and TTY states run no animation timer. + +The [[file:prototypes/2026-07-19-music-config-ui-remodel-benchmark.org][prototype benchmark]] measured full-image and tiled refresh for one minute +each at the real dock size. Full refresh failed at 133.616 ms median and +139.822 ms p95. A display-sized lower-deck tile passed at 35.513 ms median and +38.221 ms p95. The application therefore uses three aligned images: upper +player glass, lower control deck, and playlist. Each image includes the +matching raster crop. Meter ticks refresh only the lower deck; hooks refresh +the other regions. The source geometry and user-visible layout do not change. + +** Errors and empty states + +An empty queue shows the receiver, =UNTITLED=, =0 TRACKS=, and fourteen blank +rows. Playlist actions and radio search remain enabled. Transport, seek, and +mode actions that require a track are visibly inactive and report a short +message if invoked by key. + +Missing artwork uses the existing vinyl placeholder. Missing metadata is +omitted. The UI never displays =Unknown=, =N/A=, a raw nil, or a raw stream URL +as program information. + +If SVG, librsvg, the raster shell, or a minimum legible viewport is unavailable, +F10 opens the implemented fancy/text playlist instead and reports why once. If +mpv IPC is unavailable, seek, volume telemetry, and meters disable without +stopping playback. A command reports the failed operation and the next step, +for example: =Music seek unavailable: mpv IPC socket is not connected; start a +track and try again.= + +=M-x cj/music-ui-doctor= reports graphical/SVG support, resolved assets and font, +view size, last render time, mpv socket state, meter filter state, telemetry +timer state, and the last telemetry error. It does not log private file paths +or stream URLs unless called with a debug prefix. + +* Alternatives Considered + +** Hybrid raster skin plus dynamic SVG (chosen) + +- Good, because it preserves the leather, glass, brass grain, and machined + highlights from the approved board while keeping every changing value honest. +- Bad, because the neutral raster source and SVG geometry must stay aligned. +- Neutral, because the result is still one SVG application document even though + the document embeds raster assets. + +** Rebuild the complete receiver as vector geometry + +- Good, because every surface would scale without raster limits and theme tokens + could recolor everything. +- Bad, because reproducing leather, brushed brass, glass, and irregular reflected + light would take much longer and would look less like the approved board. +- Neutral, because simple Dupre controls still supply useful vector geometry + inside the hybrid renderer. + +** Use the final PNG as one clickable background + +- Good, because it would match the approved image immediately. +- Bad, because the PNG contains one track, one playlist, lit volume segments, + meter needles, progress, and button state. Painting over those areas would + flatten the material texture and produce visible patches. +- Neutral, because a cleaned neutral derivative of the PNG is the chosen shell. + +** Browser/xwidget application + +- Good, because browser SVG supports native DOM events and partial element + updates. +- Bad, because it introduces a browser runtime inside Emacs, complicates window + integration, and weakens TTY and configuration portability. +- Neutral, because browser prototypes remain useful before the svg.el port. + +** Replace the EMMS playlist buffer in place + +- Good, because no second buffer name or adapter is needed. +- Bad, because EMMS commands depend on point and text markers while the SVG is + one displayed image glyph. Mixing invisible rows and image coordinates would + be brittle. +- Neutral, because the dedicated view still delegates every operation to the + existing EMMS buffer in v1. + +* Decisions [10/10] + +** DONE Hybrid raster/SVG rendering +- Context: the final board's realism comes from material texture and reflected + light, while live state must remain replaceable. +- Decision: We will embed a neutral raster skin in an SVG scene and render all + changing information, needles, lights, and selection as SVG. +- Consequences: easier, the running UI can closely match the board. Harder, the + asset export and SVG geometry need one measured coordinate system. + +** DONE Craig's final board is the visual authority +- Context: the design session produced many useful directions, but implementation + needs one source for proportion and material treatment. +- Decision: We will use concept 30, Craig's 1916×821 GIMP refinement, as the + layout and appearance authority. +- Consequences: easier, visual disputes can be checked against one board. Harder, + the radio state must be derived without drifting from that hardware. + +** DONE Neutral physical shell +- Context: the final flattened PNG contains transient information and active + state. +- Decision: We will create an editable neutral source and export a clean shell, + control sprites, and neutral VU face. No live state remains in those assets. +- Consequences: easier, texture stays realistic. Harder, Phase 1 includes careful + image cleanup before application code can look finished. + +** DONE Dedicated graphical view buffer +- Context: EMMS playlist operations are point/marker based and cannot treat one + SVG image as fourteen text rows. +- Decision: We will render into =*Music Player*= and use controller adapters to + operate on the hidden EMMS playlist buffer. +- Consequences: easier, the SVG renderer stays clean and future-backend ready. + Harder, selection and reorder commands need explicit index-to-marker adapters. + +** DONE One geometry table for drawing and input +- Context: separate hand-maintained visual and hit maps will drift. +- Decision: We will measure the final board once and derive rendering, hit tests, + clipping, and scaling from the same constants. +- Consequences: easier, controls stay clickable after scaling. Harder, geometry + extraction is a named deliverable rather than ad hoc coordinates in draw code. + +** DONE Direct control semantics +- Context: every faceplate control must perform real music-config behavior. +- Decision: We will map the approved transport, modes, radio search, playlist + actions, seek, and volume controls directly to their existing commands or the + named mpv additions in this spec. +- Consequences: easier, the interface is self-explanatory. Harder, commands not + approved for the faceplate remain keyboard/M-x only and need discoverable help. + +** DONE Persistent per-player volume +- Context: the existing +/- path can change the system mixer, while the design's + knob is the player's volume and mpv restarts for each track. +- Decision: We will store volume in music-config, write mpv's volume property, + and apply the stored value when every new mpv process starts. +- Consequences: easier, the knob is truthful and track changes preserve level. + Harder, one more piece of runtime state must stay synchronized with mpv. + +** DONE Real meter data through mpv astats +- Context: random needle animation would violate the design's functional-lighting + rule. +- Decision: We will read left/right RMS metadata from a labeled FFmpeg astats + filter over mpv IPC and apply physical needle smoothing. +- Consequences: easier, the VUs convey real program level. Harder, a visible + view needs a small persistent IPC telemetry client and bounded refresh timer. + +** DONE Current text views remain fallbacks +- Context: TTY frames and SVG/asset failures need a usable player, and the + implemented fancy text UI already works. +- Decision: We will add an SVG enable flag and fall back to the current fancy or + plain text path without changing M3U or playback state. +- Consequences: easier, rollout and rollback are safe. Harder, the text path + remains maintained alongside the SVG application. + +** DONE Three-region render path after measurement +- Context: =svg-possibly-update-image= rerasterizes an SVG image, so ten full + receiver renders per second may exceed the dock's CPU budget. +- Decision: The prototype benchmark measured the full receiver at 133.616 ms + median / 139.822 ms p95 and the display-sized lower-deck tile at 35.513 ms + median / 38.221 ms p95. We will use three aligned regions and refresh only + the lower deck at meter cadence. +- Consequences: easier, meter animation passes the render budget with margin. + Harder, the renderer places three images and the asset export must hide their + boundaries exactly. + +* Review findings [0/1] + +** TODO The final interaction model has no working two-state prototype :blocking: + +The design session produced extensive static boards and established concept 30 +as the visual authority, but the final geometry has not been exercised as a +working local-playlist and radio prototype. That leaves click targets, drag +behavior, playlist scrolling, long-text clipping, local/radio state changes, +and the full-image render budget unproven. Starting implementation now would +make the first build phase answer product and performance questions that belong +in the design gate. + +Smallest resolution: create one functional prototype using concept 30's +geometry and neutral skin. It may be a browser prototype or an Emacs SVG proof, +but it must switch between a long local playlist and a live-radio fixture; wire +transport, modes, radio search, playlist actions, row and scrollbar navigation, +seek, and volume interaction; show all pressed and latched states; stress long +metadata; and record one minute of render timing at the intended dock size. +Link the accepted prototype under Prototype iterations and Design, record any +geometry or performance changes here, then repeat spec review. + +Progress on 2026-07-19: [[file:prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html][prototype 1]] now supplies both fixtures and the named +interactions; its twelve-check behavior pass is green. The [[file:prototypes/2026-07-19-music-config-ui-remodel-benchmark.org][librsvg benchmark]] +records a full-render failure and a passing lower-deck tile, so the spec now +requires the three-region renderer. Craig's hands-on acceptance is the only +remaining part of this finding. + +* Implementation phases + +** Phase 1 — Neutral skin and geometry manifest + +Create the editable neutral source from concept 30. Export aligned upper-glass, +lower-deck, and playlist crops plus control sprites and the VU face under final +dated asset names. Record measured +source rectangles, anchors, clip boxes, and hit targets in one Lisp geometry +table. Add an asset/geometry debug command that renders target outlines over the +skin. The phase ends with a static SVG in Emacs that matches the final board and +contains no baked live state. + +** Phase 2 — Pure SVG renderer and view mode + +Add =modules/music-svg-ui.el=, the state structs, pure layout helpers, raster +embedding cache, text clipping, dynamic node construction, and +=cj/music-svg-mode=. F10 can show/hide the static graphical view while the +existing fancy/text path remains available. Add unit tests for scale transforms, +hit testing, truncation, scrollbar math, mode colors, and VU angle mapping. + +** Phase 3 — Controller snapshot and playlist viewport + +Build state snapshots from EMMS without exposing EMMS objects to the renderer. +Render metadata, album art, header, fourteen rows, selection, and the exact +scrollbar. Implement keyboard and mouse scrolling, row selection/play, and +index-to-EMMS-marker adapters. Characterize existing reorder/removal behavior +before routing those keys through the graphical view. + +** Phase 4 — Transport, modes, radio search, and playlist actions + +Wire the approved faceplate controls and pressed/latched rendering. Preserve +the remaining keyboard-only commands and add a concise help surface. Cover +empty-queue behavior, destructive Delete confirmation, command errors, and +state refresh hooks. The phase ends with a complete functional control surface +using the existing backend. + +** Phase 5 — Seek and persistent mpv volume + +Add player-owned volume state and apply it on process start. Route +/−, wheel, +click, and drag through the same volume setters. Add absolute seek and drag +confirmation using mpv time/duration. Render local progress and the exact brass +thumb. Unit-test value/coordinate mapping and use a fake IPC ledger for command +order. + +** Phase 6 — Stereo telemetry and timed refresh + +Add the labeled astats filter, persistent nonblocking telemetry connection, +JSON request/reply routing, meter parsing, dB mapping, and needle ballistics. +Run progress and meter timers only while visible and active. Measure the render +budget for one minute and use the documented three-region fallback if needed. +Test with the synthetic no-audio-output source used during spec research. + +** Phase 7 — Radio state, failures, and diagnostics + +Render the matched live-radio state with exactly two ON AIR labels and no seek +surface. Handle missing art/metadata, unavailable SVG/assets/font, lost mpv IPC, +missing meter metadata, hidden-window timer shutdown, and an empty queue. Add +=cj/music-ui-doctor= and verify that fallback never mutates playback or playlist +state. + +** Phase 8 — Integration, documentation, and live verification + +Run the focused music tests, batch load, byte compilation, TTY fallback, and GUI +daemon verification. Exercise a long local playlist, a short radio playlist, +seek, volume across track changes, every control, scrolling, reorder, deletion, +and one-hour timer stability. Update music documentation and key hints. Make +the SVG view the graphical default only after Craig accepts the live result. + +* Acceptance criteria + +- [ ] F10 opens the Dupre Studios receiver in the bottom side window on a + graphical frame and toggles it closed without changing playback. +- [ ] The running receiver matches concept 30 in proportion, material character, + spacing, typography, and control placement at the normal dock size. +- [ ] Leather, glass, brass, fixed engraving, and neutral hardware come from the + raster skin; live information and illumination are not baked into it. +- [ ] The UI remains fully usable from the keyboard. +- [ ] Every visible button maps to the command named in this spec. +- [ ] Pressed, playing, and latched mode states are physically and chromatically + distinct. +- [ ] Local metadata uses separate lines and omits missing values. No visible + program field says Unknown or N/A. +- [ ] Long metadata and playlist rows truncate with an ellipsis before colliding + with adjacent fields. +- [ ] The analog header card shows only the collection/search name and item + count; it never adds a PLAYLIST or RADIO type label. +- [ ] Champagne surfaces use a fine low-contrast metallic micro-brush and never + read as wood grain. +- [ ] The playlist shows fourteen one-line rows with artist inline, selected row + amber, and a functional scrollbar whose size and position follow the formulas. +- [ ] Mouse wheel, keyboard navigation, scrollbar drag, row click/play, reorder, + and removal operate on the intended EMMS tracks. +- [ ] ADD, NEW, LOAD, SAVE, and DELETE preserve current playlist/M3U semantics. +- [ ] DELETE uses the existing strong confirmation and reports which playlist was + deleted. +- [ ] A local track displays actual elapsed/duration and supports click/drag seek. +- [ ] The seek thumb is brass and its position comes from mpv state. +- [ ] A live station has no slider or times and shows exactly two ON AIR labels. +- [ ] The volume knob controls mpv, not the system mixer, and persists across + track changes. +- [ ] Volume segments fill cumulatively to the stored mpv volume and have no + numeric display. +- [ ] VU needles use distinct real left/right RMS values, rest on missing data, + and never use random animation. +- [ ] Paused, stopped, hidden, and TTY states run no meter/progress timer. +- [ ] Full or tiled rendering meets the median/95th-percentile budget during a + one-minute meter run. +- [ ] Empty playlist, missing art, missing metadata, missing SVG/assets, and lost + mpv IPC each have the fallback or actionable error defined in this spec. +- [ ] =cj/music-ui-doctor= reports the renderer and telemetry state without + exposing file paths or stream URLs by default. +- [ ] Setting the SVG enable flag off restores the implemented fancy/text UI. +- [ ] Existing music-config tests pass, and new pure tests cover geometry, + snapshots, hit targets, clipping, scrolling, volume, seek, meter mapping, and + timer lifecycle. +- [ ] Craig accepts both a long local-playlist state and a live-radio state in the + real Emacs side window before the SVG view becomes the default. + +* Readiness dimensions + +- Data model & ownership: EMMS and M3U remain authoritative. The SVG snapshot, + pressed target, viewport offset, meter smoothing, and render timing are + generated runtime state. Player volume is music-config runtime state applied + to each mpv process. Raster source assets are project-authored and versioned. +- Errors, empty states & failure: defined in the Errors and empty states section. + Playback continues if rendering or meter telemetry fails. Destructive + playlist deletion retains the existing strong confirmation. +- Security & privacy: no credentials are added. Diagnostics omit local paths and + stream URLs by default. Album art and radio favicons stay in the existing + local cache. +- Observability: =cj/music-ui-doctor= exposes assets, font, viewport, render + timing, socket/filter/timer state, and last telemetry error. The UI itself + exposes player state through lamps, needles, progress, and selection. +- Performance & scale: fourteen visible rows from playlists tested through at + least 1,000 tracks. SVG refresh has explicit median/95th-percentile budgets, + a one-minute test, and a pre-approved tiled fallback. Hidden views do no timed + work. +- Reuse & lost opportunities: reuses the current music commands, EMMS state, + M3U/radio/art code, bottom side window, Dupre tokens, =svg.el=, and the gallery + widget proof. It deliberately does not wait for the EMMS-free/VAMP rewrite. +- Architecture fit & weak points: new renderer module is pure at its boundary; + music-config owns EMMS adaptation and mpv effects. Weak points are raster/SVG + alignment, point-based EMMS adapters, full-image rerasterization, and telemetry + lifecycle. Geometry sharing, adapter tests, tiled fallback, and visibility- + scoped IPC mitigate them. +- Config surface: add =cj/music-svg-ui= (boolean, default off until live acceptance, + then on) and =cj/music-player-volume= (integer 0–100, default 100). Keep + =cj/music-fancy-ui= as the fallback selector. Refresh rates and geometry are + internal until real use proves a customization need. +- Documentation plan: update music-config commentary/docstrings, F10/keybinding + notes, the music VERIFY task, and the design README. The final spec and dated + runtime assets remain linked from the parent task. +- Dev tooling: use the existing ERT/byte-compile commands, focused music test + files, Emacs daemon live verification, and a deterministic synthetic mpv meter + script/fixture with =ao=null=. No network or audible output in the normal + suite. +- Rollout, compatibility & rollback: SVG is additive and initially disabled. + Fallback is automatic. Rollback is setting =cj/music-svg-ui= nil or reverting + the new renderer/controller changes; M3U files and cached art do not migrate. +- External APIs & deps: built-in Emacs 30.2 =svg.el= and librsvg are the render + path. mpv 0.41 and FFmpeg 8.1 were checked locally. Official mpv and FFmpeg + documentation confirms labeled audio-filter metadata and astats keys. + +* Risks, Rabbit Holes, and Drawbacks + +- Cleaning a neutral skin from a flattened final board can produce visible + texture patches. Keep the layered source, use broad material samples rather + than small clone spots, and approve the neutral shell before wiring state. +- Full-image librsvg refresh may be too expensive at meter cadence. Measure in + the prototype. Use the specified three-region split rather than lowering the + meter to a decorative crawl. +- EMMS commands expect point in the playlist buffer. Keep every index-to-marker + translation in one controller adapter and test reorder/remove against the same + logical track. +- SVG text metrics can differ from Emacs face metrics. Use the installed Dupre + fixed-width font, fixed boxes, clipping paths, and visual stress fixtures with + long titles. +- An mpv filter may fail for an unusual audio format. Playback wins: leave the + meter at rest, show the failure in the doctor, and never stop the track. +- The raster skin limits infinite scaling and alternate themes. V1 targets the + current dock and approved aesthetic. Multiple skins and responsive reflow are + separate work. + +* Testing / Verification / Rollout + +Pure ERT tests cover layout transforms, geometry/hit agreement, text fitting, +playlist window/scrollbar math, state-to-color mapping, volume/seek conversion, +VU dB/angle mapping, ballistics, and timer state. Controller tests use an EMMS +fixture or stubs and an mpv IPC ledger so they do not play sound. + +Integration tests build representative local and radio snapshots, render SVG +strings, and assert required IDs, text, clip paths, and state classes. They do +not use pixel-perfect screenshots as the main contract because librsvg/font +versions can change antialiasing. A small set of manual reference screenshots +supports the visual acceptance step. + +The live pass runs in the real bottom side window with the normal long playlist +and a radio playlist. It covers every faceplate control, keyboard parity, +scrolling, long strings, seek, persistent volume, two-channel meters, exactly two +ON AIR states, hidden-window timer shutdown, and fallback. The SVG flag changes +to default-on only after that pass. + +* References / Appendix + +- [[file:../../docs/specs/2026-07-06-fancy-music-player-ui-spec.org][Implemented fancy music-player UI spec]] +- [[file:../../docs/specs/music-config-without-emms-spec.org][Music-config without EMMS architecture spec]] +- [[file:README.org][Music SVG design record]] +- [[file:concepts/30a-dupre-studios-user-refined-playlist.png][Near-final playlist board]] +- [[file:prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html][Functional two-state prototype]] +- [[file:prototypes/2026-07-19-music-config-ui-remodel-benchmark.org][Prototype librsvg benchmark]] +- [[https://www.gnu.org/software/emacs/manual/html_node/elisp/SVG-Images.html][Emacs Lisp SVG images]] +- [[https://mpv.io/manual/master/#json-ipc][mpv JSON IPC]] +- [[https://mpv.io/manual/master/#command-interface-af-metadata][mpv audio-filter metadata]] +- [[https://ffmpeg.org/ffmpeg-filters.html#astats-1][FFmpeg astats]] + +* Review and iteration history + +** 2026-07-19 Sun @ 19:11:57 -0500 — Codex (emacs-d) — hardware-depth correction +- What: changed the upper boundary of the lower deck from a raised champagne + rim to a dark glass reveal descending into the brass faceplate. Unified all + four play-mode and both radio-search controls as blank, circularly machined + brass pushbutton caps. Removed the returned search glyphs and expanded then + tightened the source crop to preserve the volume knob's complete black + knurled skirt without importing the old faceplate halo. +- Why: the prior neutral-faceplate overlay flattened the glass/metal depth, + allowed the small-control treatments to diverge, and clipped the knob at a + perfect circle that hid its physical knurling. These changes restore a + believable assembly hierarchy while retaining state-owned lamps. +- Artifacts: updated [[file:prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html][functional prototype 1]]; twelve-check interaction regression green. + +** 2026-07-19 Sun @ 18:43:00 -0500 — Codex (emacs-d) — prototype refinement +- What: replaced the baked lower-deck faceplate with a neutral SVG-owned + champagne micro-brush surface, then restored only the approved volume knob + through a circular crop. Re-engraved the fixed control labels on the new + surface and reduced the volume arc to small dark wells with state-owned amber + cores and local glow. Verified both local and radio layouts and reran the + twelve-check headless interaction pass successfully. +- Why: masking individual source-image lamps left visible baked illumination and + made the faceplate look patched. Owning the complete neutral deck surface in + SVG removes the conflicting lamp state and the woodlike raster grain while + retaining the realistic machined knob from the approved visual authority. +- Artifacts: updated [[file:prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html][functional prototype 1]]; local and radio state screenshots. + +** 2026-07-19 Sun @ 18:39:41 -0500 — Codex (emacs-d) — prototype refinement +- What: applied Craig's second hands-on pass. Removed source-type words and the + separator from the analog card; changed radio fixture naming to =NAME SEARCH=; + replaced the circular seek ornament with a vertical machined-brass grip; + removed all mode-cap glyphs and matched their smooth spun-brass finish to the + volume cap; and rebuilt the volume arc as dark recessed wells whose amber core + and glow exist only for lit positions. +- Why: the card should identify the current collection/search rather than report + an internal playlist/radio type. The prior seek coin had no hardware model, + the mode symbols duplicated their faceplate labels, and baked light spill made + some nominally dark volume positions glow. The spec now also rejects coarse, + wavy champagne texture that reads as wood grain. +- Artifacts: updated [[file:prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html][functional prototype 1]]; Craig's 18:29–18:32 screenshots. + +** 2026-07-19 Sun @ 18:19:28 -0500 — Codex (emacs-d) — prototype refinement +- What: incorporated Craig's first hands-on notes. Rebuilt play modes as + alternate-action brass pushbuttons with engraved glyphs, visible travel, and + a restrained inset green ring; tightened Play/Pause illumination; aligned + each volume lamp's dark and lit state to one physical well; restored four + concealed warm-light pools on a darker coffee/cream playlist card; and made + the prototype's synthetic-VU / production-signal distinction explicit. +- Why: the prior controls read as glowing domes, the green looked like a neon + outline, the flattened source and live volume arcs produced double lamps, and + the playlist card had lost its analog illumination model. Production VUs + remain tied to real mpv astats RMS data as already specified. +- Artifacts: updated [[file:prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html][functional prototype 1]]; Craig's 17:59–18:03 screenshots. + +** 2026-07-19 Sun @ 17:56:27 -0500 — Codex (emacs-d) — prototype author +- What: built functional prototype 1 from concept 30 with local and radio + fixtures, all approved faceplate controls, long-string stress data, exactly + fourteen visible rows, scrollbar and seek/volume drag, pressed/latched states, + empty/delete flows, keyboard navigation, and synthetic stereo needle motion. + Its built-in twelve-check behavior pass is green. Measured librsvg for one + minute per path: full receiver 133.616 ms median / 139.822 ms p95 (fail), + display-sized lower deck 35.513 ms median / 38.221 ms p95 (pass). +- Why: this supplies the interaction and performance evidence requested by the + first review finding. The measured result makes the three-region renderer a + requirement rather than a contingency. Craig's hands-on prototype acceptance + remains pending, so the finding stays open and the spec stays DRAFT. +- Artifacts: [[file:prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html][functional prototype 1]]; [[file:prototypes/2026-07-19-music-config-ui-remodel-benchmark.org][benchmark record]]; + [[file:2026-07-19-music-config-ui-remodel-render-fixture.svg][full-render fixture template]]. + +** 2026-07-19 Sun @ 16:16:57 -0500 — Codex (emacs-d) — reviewer +- What: completed the first implementation-readiness review after reading the + current music-config render, EMMS playlist, radio, mpv IPC, and volume paths. + Assigned =Not ready= and recorded one blocking finding: the approved final + board is static, so its two application states, hit/drag/scroll behavior, and + rendering budget have not passed the project's functional-prototype gate. +- Why: the architecture, ownership, failure handling, telemetry source, rollout, + acceptance criteria, and eight phase boundaries are otherwise specific enough + to implement. A working prototype is the smallest remaining test of the + design rather than an invitation to redesign it during Phase 1. +- Artifacts: =* Review findings [0/1]= in this spec; source checks in + =modules/music-config.el=; Dupre =gallery-widget.el= SVG proof; verified mpv + =af-metadata/meter= / FFmpeg =astats= telemetry path. + +** 2026-07-19 Sun @ 16:03:13 -0500 — Codex (emacs-d) — author +- What: drafted the Music Config UI Remodel spec from Craig's selected concept, + manual GIMP refinement, and hybrid raster/SVG decision. Verified the real VU + telemetry path against the installed mpv/FFmpeg stack and primary docs. +- Why: the visual design is settled closely enough to define the renderer, + controller boundary, interaction surface, phases, and test contract before + implementation. +- Artifacts: [[file:concepts/30a-dupre-studios-user-refined-playlist.png][concept 30]]; local mpv astats IPC check; parent music SVG task. diff --git a/working/music-svg-app-design/README.org b/working/music-svg-app-design/README.org new file mode 100644 index 00000000..5ef6d67d --- /dev/null +++ b/working/music-svg-app-design/README.org @@ -0,0 +1,426 @@ +#+TITLE: Music SVG Application Design Working Set +#+AUTHOR: Craig Jennings + +* Purpose + +Six functional HTML/SVG prototypes for the music-config application design session. +They explore the Dupre chronometer, hi-fi stereo, and automobile-dashboard visual +language while preserving the current player, queue, radio, and playlist workflows. + +This directory owns the complete in-progress remodel package while the design +continues. The implementation spec is +[[file:2026-07-19-music-config-ui-remodel-spec.org][2026-07-19-music-config-ui-remodel-spec.org]]. +The current interactive and rendering evidence lives under [[file:prototypes/][prototypes/]]. +Concept boards, source artwork, and visual references stay beside them so local +iteration does not depend on files scattered through =docs/=. When the design +is accepted, the spec, prototypes, benchmark, render fixture, and selected final +assets graduate together to their permanent =docs/= and =assets/= homes. + +The selected receiver now has a final interaction candidate at +[[file:prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html][prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html]]. It uses +concept 30 as the physical shell and overlays the changing local-playlist and +radio state. The paired [[file:prototypes/2026-07-19-music-config-ui-remodel-benchmark.org][librsvg benchmark]] makes the three-region production +renderer mandatory: full-instrument meter refresh missed the budget, while the +lower control tile passed. + +* Inputs + +- [[file:../../docs/specs/2026-07-06-fancy-music-player-ui-spec.org][Existing music player UI spec]] +- [[file:../../docs/specs/music-config-without-emms-spec.org][EMMS-free architecture spec]] +- [[file:../../../code/archsetup/docs/prototypes/panel-widget-gallery.html][Dupre panel-widget gallery]] +- [[file:../../../code/archsetup/docs/prototypes/waybar-redesign-prototype.html][Waybar redesign prototype]] + +* Prototype Directions + +1. Gold-pinstripe tuner console +2. Touring-car instrument cluster +3. Chronometer record deck +4. Studio rack and tape machine +5. Perpetual-calendar salon receiver +6. Mission-control radio navigator + +* High-Fidelity Concept Boards — Iteration Two + +The second pass deliberately separates visual design from implementation. These +boards establish materials, lighting, silhouette, hierarchy, and instrument +semantics before the selected direction is translated into Dupre components. + +1. [[file:concepts/01-champagne-receiver.png][Champagne Receiver]] +2. [[file:concepts/02-le-mans-night-cluster.png][Le Mans Night Cluster]] +3. [[file:concepts/03-geneva-playback-chronograph.png][Geneva Playback Chronograph]] +4. [[file:concepts/04-mastering-room-reel-console.png][Mastering Room Reel Console]] +5. [[file:concepts/05-perpetual-calendar-salon.png][Perpetual Calendar Salon]] +6. [[file:concepts/06-transatlantic-broadcast-navigator.png][Transatlantic Broadcast Navigator]] +7. [[file:concepts/07-functional-black-glass-receiver.png][Functional Black Glass Receiver]] +8. [[file:concepts/08-functional-black-glass-flipped.png][Functional Black Glass Receiver — flipped]] +9. [[file:concepts/09-functional-black-glass-retro.png][Functional Black Glass Receiver — retro]] +10. [[file:concepts/10-warm-black-glass-player-volume.png][Warm Black Glass Receiver — player volume]] +11. [[file:concepts/11-illuminated-black-glass-controls.png][Illuminated Black Glass Receiver — varied controls]] +12. [[file:concepts/12-consolidated-functional-receiver.png][Consolidated Functional Receiver]] +13. [[file:concepts/13-black-silver-chronograph-receiver.png][Black and Silver Chronograph Receiver]] +14. [[file:concepts/14-corrected-luxury-chronograph-receiver.png][Corrected Luxury Chronograph Receiver]] +15. [[file:concepts/15-champagne-brass-digital-hifi.png][Champagne-Brass Digital Hi-Fi]] +16. [[file:concepts/16-champagne-aluminum-radio-groups.png][Champagne-Aluminum Radio Groups]] +17. [[file:concepts/17-dupre-coltrane-scrollbar.png][Dupre Coltrane Scrollbar State]] +18. [[file:concepts/18-dupre-branding-control-study.png][Dupre Branding and Compact-Control Study]] +19. [[file:concepts/19a-dupre-playlist-state.png][Dupre Playlist State]] / [[file:concepts/19b-dupre-radio-state.png][Dupre Radio State]] +20. [[file:concepts/20-dupre-playlist-radio-comparison.png][Dupre Long-Playlist / Compact-Radio Comparison]] ([[file:concepts/20a-dupre-long-playlist.png][playlist source]] / [[file:concepts/20b-dupre-compact-radio.png][radio source]]) +21. [[file:concepts/21-dupre-semantic-controls-comparison.png][Dupre Semantic-Controls Comparison]] ([[file:concepts/21a-dupre-mode-lamps-playlist.png][playlist source]] / [[file:concepts/21b-dupre-two-on-air-radio.png][radio source]]) + +- [[file:concept-board.html][Open the six-board review sheet]] + +The original functional HTML pass remains at +[[file:music-svg-directions.html][music-svg-directions.html]] for interaction and +workflow reference; it is not the material-design target. + +* Current Design Decisions + +- Beauty must communicate real application function; no decorative controls. +- Preserve the Champagne Receiver's now-playing, progress, transport, queue, + and playlist-management regions as the functional nucleus. +- Remove AM/FM, tone, speaker, loudness, power, and other receiver controls + without a =music-config.el= command or state. +- Target a compact panoramic panel in the bottom portion of a predominantly + portrait Emacs frame; playlist and player controls must sit side by side. +- Prefer a neutral black-glass, warm-meter, polished-chrome language with + restrained warm metal and tactile black leather; keep whitespace low without + returning to clutter. Do not use cold blue illumination. +- Permanent radio controls map only to the implemented name and tag searches. + Manual URL entry remains an Emacs command rather than faceplate hardware. +- Place the player on the left and playlist on the right. +- The dots around the volume knob are a cumulative illuminated scale driven by + actual volume; the last lit dot is the precise pointer. They never chase or + flash. +- The knob controls dedicated mpv player volume rather than the system mixer. + Persist the level in Emacs, pass it to each newly started per-track mpv + process, and read/write mpv's =volume= property over the existing IPC socket. +- Drive the stereo VU meter from real mpv audio analysis. A labeled lavfi + =astats= filter exposes per-channel RMS and peak readings through + =af-metadata/<label>=; apply analog needle ballistics in the UI. Treat the + meter as pre-volume program level so it describes the recording independently + of listening volume. +- Prefer neutral black glass, chrome, and pebbled leather with warm ivory, + amber, and restrained jewel illumination; avoid cold blue light. +- All playlist and now-playing typography is self-luminous behind smoked glass, + never merely engraved. Compose album metadata conditionally from album, + label, year/date, genre, composer, performer, track, and disc; omit missing + values and separators, and never display =Unknown= or =N/A=. +- Do not display a numeric player-volume value. The physical knob pointer, + ticks, and cumulative lamps communicate level. +- Omit manual URL entry and playlist reload from the permanent faceplate; the + commands remain available through Emacs. +- For seekable tracks, the progress indicator is an interactive slider. For a + non-seekable live stream, replace the entire slider/time treatment with an + =ON AIR= state; never show both states together. +- With the slider handling seeking, transport contains only previous, + play/pause, stop, and next. Labels live on the faceplate beneath compact + physical controls. +- Put repeat, single, random, and consume lamps/toggles on the lower player + control rail. Put radio name/tag search there as well. The right side is + reserved for the large playlist and visible playlist actions. +- Present now-playing metadata as separate illuminated lines and omit the + =NOW PLAYING= caption. +- Use one consistent family of small round chrome transport pushbuttons. Mode + controls use four identical low-profile luxury latching pushbuttons with + polished-steel collars, smoked-black enamel centers, and integrated faceted + jewels: pressed and lit means on; raised and dark means off. Bat toggles are + prohibited because they read as utilitarian rather than luxury hardware. +- Build depth with a realistic two-step polished-chrome bezel and dark gasket + around both the VU meter and complete player where they meet black leather. + Avoid excessive parallel chrome rails. +- Use a coherent brushed-silver lower control faceplate. Chrome and silver + dominate; brass remains a hairline accent. +- The playlist shows roughly fourteen compact one-line rows with artist inline, + an explicit illuminated playlist name, and square matte-anodized action keys. + Visible actions are add, new, load, save, and delete. Delete is a normal + matte-red key; edit is not permanent faceplate functionality. +- Put the =ON AIR= lamp inside the radio control bank. It remains dark for a + local file and lights only for a live radio source. +- Do not use the drum/tape roller as a saved-playlist selector. Its unlabeled + adjacent digits were mistaken for a counter and its purpose was not legible; + the labeled =LOAD= action is sufficient. If a drum mechanism is revisited, + it must expose a unique function that is understood without explanation. +- Volume segments are identical and evenly spaced; light continuously from the + rightmost minimum through and including the pointer-aligned segment. +- Allocate approximately 42 percent of usable width to the player and 58 + percent to the playlist. When all fourteen rows fit, show neither a scrollbar + nor another redundant track-count mechanism. +- Remove the current-track =01 / 14= complication entirely: the highlighted + playlist row communicates the same information more directly. +- Put the mode/status/radio rail above the transport rail. Align =ON AIR= with + the other mode jewels and give it the same visible diameter, while retaining + its non-pressable status-only behavior. +- Transport hardware is a coherent bank of four shallow, closely spaced, + machined-metal hi-fi piano keys rather than separate round buttons. +- Use distinctly digital, self-luminous typography for player metadata and the + playlist; keep faceplate labels engraved rather than digital. +- A pale brushed champagne-brass control faceplate is viable so long as black + glass and polished chrome remain dominant and the brass never becomes bronze, + copper, brown, or sepia. +- Prefer authentic pale champagne-anodized aluminum over brass: fine horizontal + brushing, cool metallic highlights, and crisp machined edges retain warmth + while matching real high-end audio construction. +- Fasteners are small, discreet, countersunk details rather than prominent + corner bolts. +- Make radio membership explicit on the faceplate. A restrained engraved + =RADIO= heading and hairline bracket span exactly =ON AIR=, =BY NAME=, and + =BY TAG=; a separate =PLAY MODES= heading owns repeat, single, random, and + consume. Do not use another heavy enclosure. +- Mode controls combine an unmistakably pressable shallow aluminum cap with a + smaller inset jewel. =ON AIR= uses the same jewel diameter and baseline but + no cap or travel, preserving its status-only meaning. +- For a seekable track, slider position must be computed from elapsed/duration + rather than independently illustrated. Concept 16 still places the handle + too far right for =02:37 / 05:35=; implementation must place it at 47 percent. +- Color carries stable semantics: warm ivory is ordinary information; amber is + active playback, progress, and volume; green is an enabled playback mode; + neutral silver is keyboard focus; red is reserved for delete, live =ON AIR=, + and faults. Do not color ordinary playlist actions arbitrarily. +- Use conventional volume geometry: minimum is lower left, maximum lower right, + and illumination advances clockwise from minimum through the knob pointer. + Segments after the pointer toward maximum remain dark. +- Playing state must be physical as well as chromatic: the =PLAY/PAUSE= key sits + visibly lower with its shadow withdrawn; a restrained amber edge is secondary + confirmation. +- The faceplate may carry one modest maker's plaque: coarse black crosshatch + that survives small SVG rendering, with a raised cursive =Dupre= wordmark. + It is a badge, not a fake speaker grille, and must not justify extra height. +- For lists longer than the fourteen-row viewport, show a narrow functional + scrollbar. Thumb length represents =visible rows / total rows= and position + represents the top visible row. Concept 17 demonstrates the long-list state + but its generated thumb remains shorter than the required 14/32 (44 percent). +- The next review set is a matched two-state pair: one seekable normal-playlist + player and one live-radio player using identical hardware. The radio state + replaces slider and times with =ON AIR=, lights the red broadcast lamp, and + conditionally shows only available station/stream metadata. +- Do not assume short metadata. Recover display width by reducing album art and + VU width roughly 20 percent; allow modest automatic font condensation before + ellipsis. Do not marquee by default. Full truncated values remain available + through normal Emacs help/minibuffer affordances. +- Compact playback modes into a 2×2 internally illuminated-legend block. The + engraved symbol itself glows; do not add a separate lamp or oversized lip. + Keep explicit micro-labels because =CONSUME= has no universal symbol. +- Place a similarly compact radio block immediately beside play modes: =NAME= + and =TAG= momentary buttons above a flush =ON AIR= window. This leaves the + right side visually quiet around volume. +- Branding is part of the faceplate surface, never a plate atop another plate. + Concept 18 compares: black-enamel inscription (A), bright-cut inscription + (B), same-plane crosshatched negative-space field (C), and direct-mounted + chrome script without backing (D). +- Select concept 18 option D: a moderately enlarged direct-mounted chrome + cursive =Dupre= script at the faceplate's upper left, with no backing. Put + compact proportional =NAME=, =TAG=, and =ON AIR= radio controls below it. +- Transport adopts the same internally illuminated-legend construction as the + mode keys. The symbol itself is translucent: amber while active, dim ivory + while inactive. Do not add a separate under-key light strip. Concepts 19a/b + still contain a generated amber strip beneath Play and are non-authoritative + on that detail. +- Treat local and live playback as matched states of identical hardware. A + seekable file shows slider and times and keeps =ON AIR= dark; a live stream + removes all seek/time hardware, displays =ON AIR / LIVE STREAM= in that region, + and lights the faceplate broadcast window red. +- Scrollbar thumbs remain mathematical UI, not illustrative ornament: 14/32 is + 44 percent and 14/18 is 78 percent. Both generated concept-19 thumbs are too + short despite their otherwise useful long-list demonstrations. +- Reserve a clear brand field at the far left of the faceplate and optically + center =Dupre= within that invisible rectangle. All functional controls stay + outside it. +- Immediately right of the brand field, stack =NAME=, =TAG=, and =ON AIR= in a + narrow vertical Radio column, right-justified tightly against an even smaller + 2×2 Play Modes block. Volume retains the quiet right-hand field. +- Transport state is symbol-only illumination. The area beneath every key is + plain faceplate plus its engraved label; no amber strip, lamp, or glow. +- Stress-test metadata with realistic long values. Concept 20 uses =A CHICKEN + WITH ITS HEAD CUT OFF= / =THE MAGNETIC FIELDS= and the 69-track =69 LOVE SONGS= + list without marquee or truncation. +- Review normal and live states together by composing their independently + generated panoramic sources. This preserves the true bottom-dock form factor + while making both states simultaneously visible. +- Concept 20's scrollbar rendering remains non-authoritative: the 14/69 playlist + thumb should be 20 percent, and the 14/18 radio thumb should be 78 percent and + clearly visible. The image model made the former too short and omitted the + latter's filled thumb. +- Live playback contains exactly two =ON AIR= presentations: a red bottom-most + metadata line and the current station row. Remove the faceplate status window + and any centered banner or =LIVE STREAM= caption. +- The compact controls form a measured 3×2 grid above transport. Every small + control is half a transport key's width and equal to its height; stacked Radio + occupies column one and the 2×2 Play Modes block columns two and three. The + grid's right edge aligns with =NEXT=. +- Radio =NAME= and =TAG= are momentary search buttons with magnifying-glass + glyphs and no lamps. Persistent playback modes use a separate tiny status + lamp above a compact champagne/brass actuator, borrowing the Heston preset + language without numeric legends. +- Branding is a true faceplate aperture: champagne aluminum is cut away to + reveal a recessed dark woven/diamond-textured subplate with centered warm-white + cursive =Dupre=. It is not a badge attached atop the faceplate. +- =PLAY/PAUSE= glows green through its symbol while playing. Previous, Stop, and + Next stay neutral and merely brighten ivory during activation. +- Volume uses a deeply knurled black body and machined champagne/brass cap with no + pointer. The cumulative lamp arc is the sole indicator and its final lit + segment communicates the level. +- =ADD= and =NEW= may share a restrained OD-green anodized tint. =LOAD= and + =SAVE= remain neutral; =DELETE= remains matte red. +- Concept 21 again renders scrollbar fill incorrectly; the exact 20/78 percent + requirements remain implementation constraints rather than image guidance. +- Tone =DELETE= down from saturated red to muted, low-saturation oxblood or + burgundy. It remains unmistakably destructive without becoming the loudest + object in the application. +- Playback modes use four small circular brass pushbuttons mounted directly on + the metal. Remove the black switch tiles and separate lamps; illuminate only + a thin circumference around an active brass actuator. A single hairline + engraved boundary contains the 2×2 group and its geometry aligns with the + transport grid ending at =NEXT=. +- Split the lower player into two genuine material zones rather than stacking + branding plates. The left 28--32 percent is uninterrupted black woven grille + continuous with the chassis; center a restrained warm-ivory/champagne cursive + =Dupre= directly in that negative space with no border, plaque, or fasteners. + The right 68--72 percent is one compact self-contained champagne-anodized + control island containing Radio, Play Modes, transport, and Volume. +- Concept 22 validates this identity-bay/control-island architecture in matched + playlist and radio states. Its compact balance is authoritative; the image + model's engraved Play Modes boundary does not yet reach the =NEXT= alignment + datum, so the exact grid alignment remains an implementation constraint. +- Lock the concept-22 champagne faceplate and every component mounted on it: + Radio keys, direct-mounted brass Play Modes matrix and engraved boundary, + pointerless cumulative Volume dial, transport keys, fasteners, typography, + materials, spacing, and illumination semantics. Subsequent exploration may + change surrounding glass and layout, but not this control component. +- Swap the information and identity zones. Put reduced album/station art and + separate-line metadata in the lower black-glass bay beside the locked + faceplate. Put =Dupre= directly on the upper black glass with no backing + texture, beside substantially larger VU meters. +- Light each VU face with a subtle warm pool rising from a hidden lamp directly + below its =VU= legend. The fixture is not visible; it only explains the + meter's restrained internal illumination. +- Give the player approximately 49 percent of total width and the list 51 + percent. Keep fourteen rows; ellipsize only the title/artist field when it + exceeds its allocation, while preserving the row number, current-state marker, + duration, and live status. +- In radio state, place the first approved =ON AIR= directly underneath the + reduced station art. The second remains in the active station row; all + inactive status cells are empty. Concept 23 is authoritative for this + swapped-information arrangement. +- Divide the player itself into two equal-height tiers. The upper tier is one + continuous black-glass identity/information display; =Dupre= stays at far left + while reduced art, separate-line metadata, and conditional seek information + occupy the remaining width. +- The lower tier is one uninterrupted full-width champagne-brass faceplate, not + a small plate stacked over another material. Recess modest dual VU meters + into its left side and integrate the locked Radio, Play Modes, Volume, and + transport hardware on its right with proportional spacing. +- Return the VU meters to approximately concept-22 scale or slightly smaller. + They are black instrument windows with narrow chrome bezels physically set + into the brass; their concealed lower lamps remain a soft wash rather than a + visible point source. +- Concept 24 demonstrates this full-brass lower tier in matched playlist and + radio states. State-dependent information changes only in the upper glass + and control illumination; the underlying manufactured unit is identical. +- Remove fasteners and corner radii from every interior surface. The upper + information glass, lower brass faceplate, and right playlist are three flush + rectangular fields separated only by one horizontal and one vertical rule. + Rounded depth remains exclusively in the outer leather/chrome chassis. +- Align the left edge of album or station art exactly with the left edge of the + stacked Radio buttons below. This is a shared vertical datum and reserves a + larger uninterrupted field for =Dupre=. Confine local seek time, line, and + handle to the information block beginning at that datum; nothing extends + beneath the wordmark. Live =ON AIR= occupies the corresponding position below + station art without a seek affordance. +- Render ordinary album metadata and inactive list rows in soft neutral white. + Amber is reserved for the list header/rule and the currently selected row; + inactive rows have no lamps, glow, dots, or amber tint. +- VU windows are flush or slightly recessed apertures in the brass, with only a + hairline inlaid edge and inner shadow. They must never read as raised boxes or + chrome housings attached to the faceplate. +- Concept 25 demonstrates the seamless square-panel and white-type direction. + The image generator still places art left of the Radio datum despite a + targeted correction; exact shared-edge alignment remains authoritative for + the implemented SVG. +- Reintroduce rounded depth selectively rather than through internal cards. The + two-step chrome trim encircling the complete unit has clearly rounded corners, + and the flush VU apertures may use modest rounded corners. +- Rename the maker signature to a two-line =Dupre Studios=: large cursive + =Dupre= above smaller widely tracked uppercase =STUDIOS=, centered as one + optical lockup directly on the black glass. +- Move album/station art, metadata, and the conditional seek/live treatment + farther right to enlarge the calm manufacturer field. +- Remove the upper vertical player/list separator entirely. Branding, program + information, and playlist share one continuous black-glass plane; header rules + and column alignment alone establish list hierarchy. +- Define the lower brass faceplate as a distinct receiver module without adding + fasteners: a narrow recessed dark groove followed by slim deep chrome encircles + all four sides of the brass. Its surround uses graceful rounded corners while + the single brass field remains uninterrupted. +- Concept 26 demonstrates the =Dupre Studios= lockup, continuous upper glass, + rounded outer chrome, rounded flush VU apertures, and fully encircled brass + control module in matched playlist and radio states. +- Remove the complete unit's outer metal trim. The exterior silhouette is a + softly rolled black-leather edge; the leather terminates directly against the + continuous black glass through a narrow compressed dark seam, with no metal + between those materials. +- Convert every remaining metal edge from chrome to warm brass. This includes + the bead encircling the lower faceplate and the hairline rounded VU aperture + edges. No silver/chrome edging remains anywhere. +- The artwork, stacked Radio keys, and =PREV= transport key share one exact left + edge. Treat this as a single vertical construction datum; the seek assembly + begins there as well and remains below only the art/metadata block. +- Make only the playlist/radio header row an analog instrument card: darker + coffee-and-cream stock behind glass with dark espresso lettering. Four hidden + warm lamps above it—one at each corner and two evenly spaced between—cast + broad subtle pools downward. Neither bulbs nor hotspots are visible, and all + list rows below remain black with white/selected-amber type. +- Concept 27 demonstrates the leather-to-glass junction, brass-only edging, and + analog illuminated header in matched playlist/radio states. Its artwork is + visually close to the control datum; implementation must make the shared edge + mathematically exact. +- Adopt Craig's GIMP faceplate composition as the control-layout authority. + Divide usable brass width into approximately 44/25/31 percent zones: dual VUs + over transport at left, Play Modes over a horizontal Name/Tag Radio pair at + center, and Volume vertically centered at right. Use shared baselines, + consistent label offsets, and even component gaps so the plate is full without + crowding. +- The four transport keys span roughly the combined width of the two VUs and sit + directly beneath them. The center controls form two aligned modules: 2×2 + direct-mounted brass modes above and two black radio search keys below. +- Increase the Volume control to the diameter in Craig's GIMP arrangement, but + make it a low-profile frontal instrument: shallow knurled skirt, broad flush + machined brass cap, minimal shadow, and no domed, magnified, or fisheye look. + Center the complete knob/arc vertically and put =VOLUME= beneath it. +- Volume lamps return to unmistakable warm amber/gold. Cumulative lit segments + must never wash out to white, cream, coral, or pink; unlit segments remain + dark. +- Concept 28 demonstrates the proportional three-zone control layout and flatter + enlarged Volume dial in matched playlist and radio states. +- Finalize the program-information construction line by moving the artwork and + all metadata right as one rigid group until the artwork's left edge is exactly + collinear with the left engraved boundary of the Radio group below. The local + seek treatment begins on that same datum: move its left time and left endpoint + with the information group, keep its right time and endpoint fixed, and shorten + the run accordingly. +- The circular seek thumb is warm polished brass, not chrome or silver. Its + restrained highlight may distinguish it from the amber progress line without + introducing a cold-metal exception to the brass-only material language. +- Treat Craig's manually refined 1916×821 playlist board, saved as + =concepts/30a-dupre-studios-user-refined-playlist.png=, as the near-final + visual authority. It resolves the upper-panel composition: the larger calm + =Dupre Studios= field, right-shifted artwork and separate metadata lines, a + contained shorter seek run with brass thumb, and a playlist-dominant right + side all coexist without crowding. +- Preserve the board's proportional lower deck: paired illuminated VUs and one + coherent transport family at left, compact direct-mounted Play Modes and Radio + Search controls in the center, and the large vertically centered low-profile + Volume instrument at right. + +* Concept 14 Review Notes + +The four matching jeweled latching mode buttons, simplified two-step chrome +bezel, fourteen-row playlist, explicit playlist name, one-line track rows, and +clearly labeled action keys move the design toward the selected language. The +board still contains three image-generation errors that are not design changes: + +- The player remains wider than the playlist instead of the required 42/58 + allocation. +- The volume arc stays illuminated beyond the knob pointer instead of ending at + and including the pointer-aligned segment. +- =COLUMBIA= and =1959= share a line instead of occupying separate conditional + metadata lines. diff --git a/working/music-svg-app-design/assets/vinyl-placeholder.svg b/working/music-svg-app-design/assets/vinyl-placeholder.svg new file mode 100644 index 00000000..cf01519f --- /dev/null +++ b/working/music-svg-app-design/assets/vinyl-placeholder.svg @@ -0,0 +1,20 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="300" height="300" viewBox="0 0 300 300" role="img" aria-label="Vinyl record placeholder"> + <defs> + <radialGradient id="disc" cx="50%" cy="50%" r="50%"> + <stop offset="0%" stop-color="#1a1a1a"/> + <stop offset="100%" stop-color="#000000"/> + </radialGradient> + </defs> + <circle cx="150" cy="150" r="148" fill="url(#disc)"/> + <g fill="none" stroke="#2b2b2b" stroke-width="1"> + <circle cx="150" cy="150" r="138"/> + <circle cx="150" cy="150" r="126"/> + <circle cx="150" cy="150" r="114"/> + <circle cx="150" cy="150" r="102"/> + <circle cx="150" cy="150" r="90"/> + <circle cx="150" cy="150" r="78"/> + </g> + <circle cx="150" cy="150" r="52" fill="#d99a2b"/> + <circle cx="150" cy="150" r="52" fill="none" stroke="#a8741a" stroke-width="2"/> + <circle cx="150" cy="150" r="7" fill="#111111"/> +</svg> diff --git a/working/music-svg-app-design/concept-board.html b/working/music-svg-app-design/concept-board.html new file mode 100644 index 00000000..378d13ea --- /dev/null +++ b/working/music-svg-app-design/concept-board.html @@ -0,0 +1,25 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width,initial-scale=1"> +<title>Music SVG application — high-fidelity concept boards</title> +<style> +:root{color-scheme:dark;--bg:#0c0b0a;--panel:#151310;--ink:#eee3c8;--dim:#97938a;--gold:#e7aa3f;--line:#40351f} +*{box-sizing:border-box}body{margin:0;background:radial-gradient(ellipse at top,#282014 0,transparent 38%),var(--bg);color:var(--ink);font:14px/1.5 "BerkeleyMono Nerd Font","Berkeley Mono",monospace} +header{max-width:1500px;margin:auto;padding:28px 24px 18px}h1{margin:4px 0 8px;color:var(--gold);font:500 clamp(24px,4vw,44px) Georgia,serif}.eyebrow{color:var(--dim);font-size:11px;letter-spacing:.22em;text-transform:uppercase}p{max-width:880px;color:var(--dim)} +main{max-width:1500px;margin:auto;padding:0 24px 70px;display:grid;gap:28px}.board{border:1px solid var(--line);border-radius:14px;overflow:hidden;background:var(--panel);box-shadow:0 18px 50px #0009}.board img{display:block;width:100%;height:auto}.caption{display:flex;gap:18px;align-items:baseline;padding:13px 17px;border-top:1px solid var(--line)}.num{color:var(--gold)}h2{font-size:15px;letter-spacing:.08em;margin:0;font-weight:500}.hint{margin-left:auto;color:var(--dim);font-size:11px}@media(max-width:700px){header,main{padding-left:12px;padding-right:12px}.caption{align-items:flex-start;flex-wrap:wrap}.hint{width:100%;margin:0}} +</style> +</head> +<body> +<header><div class="eyebrow">Music SVG application · material-design pass</div><h1>Six instruments worth living with</h1><p>Judge the silhouette, material palette, visual hierarchy, and behavior metaphors. Typography is illustrative; implementation will use the actual Dupre kit and SVG-safe primitives.</p></header> +<main> +<article class="board"><img src="concepts/01-champagne-receiver.png" alt="Champagne Receiver music application"><div class="caption"><span class="num">01</span><h2>Champagne Receiver</h2><span class="hint">hi-fi receiver · blue glass · walnut · moving-coil VU</span></div></article> +<article class="board"><img src="concepts/02-le-mans-night-cluster.png" alt="Le Mans Night Cluster music application"><div class="caption"><span class="num">02</span><h2>Le Mans Night Cluster</h2><span class="hint">grand tourer · leather · chrome gauges · tell-tales</span></div></article> +<article class="board"><img src="concepts/03-geneva-playback-chronograph.png" alt="Geneva Playback Chronograph music application"><div class="caption"><span class="num">03</span><h2>Geneva Playback Chronograph</h2><span class="hint">guilloché · sapphire chapter ring · jeweled complications</span></div></article> +<article class="board"><img src="concepts/04-mastering-room-reel-console.png" alt="Mastering Room Reel Console music application"><div class="caption"><span class="num">04</span><h2>Mastering Room Reel Console</h2><span class="hint">reel-to-reel · brushed aluminum · VU glass · walnut rack</span></div></article> +<article class="board"><img src="concepts/05-perpetual-calendar-salon.png" alt="Perpetual Calendar Salon music application"><div class="caption"><span class="num">05</span><h2>Perpetual Calendar Salon</h2><span class="hint">rosewood · ivory enamel · apertures · moonphase record</span></div></article> +<article class="board"><img src="concepts/06-transatlantic-broadcast-navigator.png" alt="Transatlantic Broadcast Navigator music application"><div class="caption"><span class="num">06</span><h2>Transatlantic Broadcast Navigator</h2><span class="hint">black glass · chrome · frequency scale · radio radar</span></div></article> +</main> +</body> +</html> diff --git a/working/music-svg-app-design/concepts/01-champagne-receiver.png b/working/music-svg-app-design/concepts/01-champagne-receiver.png Binary files differnew file mode 100644 index 00000000..453ca9e3 --- /dev/null +++ b/working/music-svg-app-design/concepts/01-champagne-receiver.png diff --git a/working/music-svg-app-design/concepts/02-le-mans-night-cluster.png b/working/music-svg-app-design/concepts/02-le-mans-night-cluster.png Binary files differnew file mode 100644 index 00000000..f967f7f6 --- /dev/null +++ b/working/music-svg-app-design/concepts/02-le-mans-night-cluster.png diff --git a/working/music-svg-app-design/concepts/03-geneva-playback-chronograph.png b/working/music-svg-app-design/concepts/03-geneva-playback-chronograph.png Binary files differnew file mode 100644 index 00000000..3990e075 --- /dev/null +++ b/working/music-svg-app-design/concepts/03-geneva-playback-chronograph.png diff --git a/working/music-svg-app-design/concepts/04-mastering-room-reel-console.png b/working/music-svg-app-design/concepts/04-mastering-room-reel-console.png Binary files differnew file mode 100644 index 00000000..3d9ced49 --- /dev/null +++ b/working/music-svg-app-design/concepts/04-mastering-room-reel-console.png diff --git a/working/music-svg-app-design/concepts/05-perpetual-calendar-salon.png b/working/music-svg-app-design/concepts/05-perpetual-calendar-salon.png Binary files differnew file mode 100644 index 00000000..301bec21 --- /dev/null +++ b/working/music-svg-app-design/concepts/05-perpetual-calendar-salon.png diff --git a/working/music-svg-app-design/concepts/06-transatlantic-broadcast-navigator.png b/working/music-svg-app-design/concepts/06-transatlantic-broadcast-navigator.png Binary files differnew file mode 100644 index 00000000..148f573f --- /dev/null +++ b/working/music-svg-app-design/concepts/06-transatlantic-broadcast-navigator.png diff --git a/working/music-svg-app-design/concepts/07-functional-black-glass-receiver.png b/working/music-svg-app-design/concepts/07-functional-black-glass-receiver.png Binary files differnew file mode 100644 index 00000000..8be7cc89 --- /dev/null +++ b/working/music-svg-app-design/concepts/07-functional-black-glass-receiver.png diff --git a/working/music-svg-app-design/concepts/08-functional-black-glass-flipped.png b/working/music-svg-app-design/concepts/08-functional-black-glass-flipped.png Binary files differnew file mode 100644 index 00000000..404c7493 --- /dev/null +++ b/working/music-svg-app-design/concepts/08-functional-black-glass-flipped.png diff --git a/working/music-svg-app-design/concepts/09-functional-black-glass-retro.png b/working/music-svg-app-design/concepts/09-functional-black-glass-retro.png Binary files differnew file mode 100644 index 00000000..12893867 --- /dev/null +++ b/working/music-svg-app-design/concepts/09-functional-black-glass-retro.png diff --git a/working/music-svg-app-design/concepts/10-warm-black-glass-player-volume.png b/working/music-svg-app-design/concepts/10-warm-black-glass-player-volume.png Binary files differnew file mode 100644 index 00000000..ae1ed9f7 --- /dev/null +++ b/working/music-svg-app-design/concepts/10-warm-black-glass-player-volume.png diff --git a/working/music-svg-app-design/concepts/11-illuminated-black-glass-controls.png b/working/music-svg-app-design/concepts/11-illuminated-black-glass-controls.png Binary files differnew file mode 100644 index 00000000..df8ce658 --- /dev/null +++ b/working/music-svg-app-design/concepts/11-illuminated-black-glass-controls.png diff --git a/working/music-svg-app-design/concepts/12-consolidated-functional-receiver.png b/working/music-svg-app-design/concepts/12-consolidated-functional-receiver.png Binary files differnew file mode 100644 index 00000000..dc2ccab4 --- /dev/null +++ b/working/music-svg-app-design/concepts/12-consolidated-functional-receiver.png diff --git a/working/music-svg-app-design/concepts/13-black-silver-chronograph-receiver.png b/working/music-svg-app-design/concepts/13-black-silver-chronograph-receiver.png Binary files differnew file mode 100644 index 00000000..2fe1af0e --- /dev/null +++ b/working/music-svg-app-design/concepts/13-black-silver-chronograph-receiver.png diff --git a/working/music-svg-app-design/concepts/14-corrected-luxury-chronograph-receiver.png b/working/music-svg-app-design/concepts/14-corrected-luxury-chronograph-receiver.png Binary files differnew file mode 100644 index 00000000..c238a8f6 --- /dev/null +++ b/working/music-svg-app-design/concepts/14-corrected-luxury-chronograph-receiver.png diff --git a/working/music-svg-app-design/concepts/15-champagne-brass-digital-hifi.png b/working/music-svg-app-design/concepts/15-champagne-brass-digital-hifi.png Binary files differnew file mode 100644 index 00000000..7469c348 --- /dev/null +++ b/working/music-svg-app-design/concepts/15-champagne-brass-digital-hifi.png diff --git a/working/music-svg-app-design/concepts/16-champagne-aluminum-radio-groups.png b/working/music-svg-app-design/concepts/16-champagne-aluminum-radio-groups.png Binary files differnew file mode 100644 index 00000000..e15bb4c5 --- /dev/null +++ b/working/music-svg-app-design/concepts/16-champagne-aluminum-radio-groups.png diff --git a/working/music-svg-app-design/concepts/17-dupre-coltrane-scrollbar.png b/working/music-svg-app-design/concepts/17-dupre-coltrane-scrollbar.png Binary files differnew file mode 100644 index 00000000..9e9e948e --- /dev/null +++ b/working/music-svg-app-design/concepts/17-dupre-coltrane-scrollbar.png diff --git a/working/music-svg-app-design/concepts/18-dupre-branding-control-study.png b/working/music-svg-app-design/concepts/18-dupre-branding-control-study.png Binary files differnew file mode 100644 index 00000000..6826f613 --- /dev/null +++ b/working/music-svg-app-design/concepts/18-dupre-branding-control-study.png diff --git a/working/music-svg-app-design/concepts/19a-dupre-playlist-state.png b/working/music-svg-app-design/concepts/19a-dupre-playlist-state.png Binary files differnew file mode 100644 index 00000000..cb08988d --- /dev/null +++ b/working/music-svg-app-design/concepts/19a-dupre-playlist-state.png diff --git a/working/music-svg-app-design/concepts/19b-dupre-radio-state.png b/working/music-svg-app-design/concepts/19b-dupre-radio-state.png Binary files differnew file mode 100644 index 00000000..26f0ff9a --- /dev/null +++ b/working/music-svg-app-design/concepts/19b-dupre-radio-state.png diff --git a/working/music-svg-app-design/concepts/20-dupre-playlist-radio-comparison.png b/working/music-svg-app-design/concepts/20-dupre-playlist-radio-comparison.png Binary files differnew file mode 100644 index 00000000..60648503 --- /dev/null +++ b/working/music-svg-app-design/concepts/20-dupre-playlist-radio-comparison.png diff --git a/working/music-svg-app-design/concepts/20a-dupre-long-playlist.png b/working/music-svg-app-design/concepts/20a-dupre-long-playlist.png Binary files differnew file mode 100644 index 00000000..b4fb84a2 --- /dev/null +++ b/working/music-svg-app-design/concepts/20a-dupre-long-playlist.png diff --git a/working/music-svg-app-design/concepts/20b-dupre-compact-radio.png b/working/music-svg-app-design/concepts/20b-dupre-compact-radio.png Binary files differnew file mode 100644 index 00000000..42992161 --- /dev/null +++ b/working/music-svg-app-design/concepts/20b-dupre-compact-radio.png diff --git a/working/music-svg-app-design/concepts/21-dupre-semantic-controls-comparison.png b/working/music-svg-app-design/concepts/21-dupre-semantic-controls-comparison.png Binary files differnew file mode 100644 index 00000000..c86f1f59 --- /dev/null +++ b/working/music-svg-app-design/concepts/21-dupre-semantic-controls-comparison.png diff --git a/working/music-svg-app-design/concepts/21a-dupre-mode-lamps-playlist.png b/working/music-svg-app-design/concepts/21a-dupre-mode-lamps-playlist.png Binary files differnew file mode 100644 index 00000000..a18fdc8b --- /dev/null +++ b/working/music-svg-app-design/concepts/21a-dupre-mode-lamps-playlist.png diff --git a/working/music-svg-app-design/concepts/21b-dupre-two-on-air-radio.png b/working/music-svg-app-design/concepts/21b-dupre-two-on-air-radio.png Binary files differnew file mode 100644 index 00000000..70303bc2 --- /dev/null +++ b/working/music-svg-app-design/concepts/21b-dupre-two-on-air-radio.png diff --git a/working/music-svg-app-design/concepts/22-dupre-identity-bay-comparison.png b/working/music-svg-app-design/concepts/22-dupre-identity-bay-comparison.png Binary files differnew file mode 100644 index 00000000..c0e7d59a --- /dev/null +++ b/working/music-svg-app-design/concepts/22-dupre-identity-bay-comparison.png diff --git a/working/music-svg-app-design/concepts/22a-dupre-identity-bay-playlist.png b/working/music-svg-app-design/concepts/22a-dupre-identity-bay-playlist.png Binary files differnew file mode 100644 index 00000000..1ab38a42 --- /dev/null +++ b/working/music-svg-app-design/concepts/22a-dupre-identity-bay-playlist.png diff --git a/working/music-svg-app-design/concepts/22b-dupre-identity-bay-radio.png b/working/music-svg-app-design/concepts/22b-dupre-identity-bay-radio.png Binary files differnew file mode 100644 index 00000000..f752ed17 --- /dev/null +++ b/working/music-svg-app-design/concepts/22b-dupre-identity-bay-radio.png diff --git a/working/music-svg-app-design/concepts/23-dupre-swapped-info-comparison.png b/working/music-svg-app-design/concepts/23-dupre-swapped-info-comparison.png Binary files differnew file mode 100644 index 00000000..252ad160 --- /dev/null +++ b/working/music-svg-app-design/concepts/23-dupre-swapped-info-comparison.png diff --git a/working/music-svg-app-design/concepts/23a-dupre-swapped-info-playlist.png b/working/music-svg-app-design/concepts/23a-dupre-swapped-info-playlist.png Binary files differnew file mode 100644 index 00000000..f9df6b85 --- /dev/null +++ b/working/music-svg-app-design/concepts/23a-dupre-swapped-info-playlist.png diff --git a/working/music-svg-app-design/concepts/23b-dupre-swapped-info-radio.png b/working/music-svg-app-design/concepts/23b-dupre-swapped-info-radio.png Binary files differnew file mode 100644 index 00000000..e532e98e --- /dev/null +++ b/working/music-svg-app-design/concepts/23b-dupre-swapped-info-radio.png diff --git a/working/music-svg-app-design/concepts/24-dupre-full-brass-vu-comparison.png b/working/music-svg-app-design/concepts/24-dupre-full-brass-vu-comparison.png Binary files differnew file mode 100644 index 00000000..285ec95f --- /dev/null +++ b/working/music-svg-app-design/concepts/24-dupre-full-brass-vu-comparison.png diff --git a/working/music-svg-app-design/concepts/24a-dupre-full-brass-vu-playlist.png b/working/music-svg-app-design/concepts/24a-dupre-full-brass-vu-playlist.png Binary files differnew file mode 100644 index 00000000..59dd8109 --- /dev/null +++ b/working/music-svg-app-design/concepts/24a-dupre-full-brass-vu-playlist.png diff --git a/working/music-svg-app-design/concepts/24b-dupre-full-brass-vu-radio.png b/working/music-svg-app-design/concepts/24b-dupre-full-brass-vu-radio.png Binary files differnew file mode 100644 index 00000000..9a0bde44 --- /dev/null +++ b/working/music-svg-app-design/concepts/24b-dupre-full-brass-vu-radio.png diff --git a/working/music-svg-app-design/concepts/25-dupre-seamless-flush-comparison.png b/working/music-svg-app-design/concepts/25-dupre-seamless-flush-comparison.png Binary files differnew file mode 100644 index 00000000..049d1dd3 --- /dev/null +++ b/working/music-svg-app-design/concepts/25-dupre-seamless-flush-comparison.png diff --git a/working/music-svg-app-design/concepts/25a-dupre-seamless-flush-playlist.png b/working/music-svg-app-design/concepts/25a-dupre-seamless-flush-playlist.png Binary files differnew file mode 100644 index 00000000..16063222 --- /dev/null +++ b/working/music-svg-app-design/concepts/25a-dupre-seamless-flush-playlist.png diff --git a/working/music-svg-app-design/concepts/25b-dupre-seamless-flush-radio.png b/working/music-svg-app-design/concepts/25b-dupre-seamless-flush-radio.png Binary files differnew file mode 100644 index 00000000..4fdbbbb8 --- /dev/null +++ b/working/music-svg-app-design/concepts/25b-dupre-seamless-flush-radio.png diff --git a/working/music-svg-app-design/concepts/26-dupre-studios-rounded-chrome-comparison.png b/working/music-svg-app-design/concepts/26-dupre-studios-rounded-chrome-comparison.png Binary files differnew file mode 100644 index 00000000..ed067d99 --- /dev/null +++ b/working/music-svg-app-design/concepts/26-dupre-studios-rounded-chrome-comparison.png diff --git a/working/music-svg-app-design/concepts/26a-dupre-studios-rounded-chrome-playlist.png b/working/music-svg-app-design/concepts/26a-dupre-studios-rounded-chrome-playlist.png Binary files differnew file mode 100644 index 00000000..b086886d --- /dev/null +++ b/working/music-svg-app-design/concepts/26a-dupre-studios-rounded-chrome-playlist.png diff --git a/working/music-svg-app-design/concepts/26b-dupre-studios-rounded-chrome-radio.png b/working/music-svg-app-design/concepts/26b-dupre-studios-rounded-chrome-radio.png Binary files differnew file mode 100644 index 00000000..dce2f048 --- /dev/null +++ b/working/music-svg-app-design/concepts/26b-dupre-studios-rounded-chrome-radio.png diff --git a/working/music-svg-app-design/concepts/27-dupre-studios-leather-analog-header-comparison.png b/working/music-svg-app-design/concepts/27-dupre-studios-leather-analog-header-comparison.png Binary files differnew file mode 100644 index 00000000..603aa5a0 --- /dev/null +++ b/working/music-svg-app-design/concepts/27-dupre-studios-leather-analog-header-comparison.png diff --git a/working/music-svg-app-design/concepts/27a-dupre-studios-leather-analog-header-playlist.png b/working/music-svg-app-design/concepts/27a-dupre-studios-leather-analog-header-playlist.png Binary files differnew file mode 100644 index 00000000..288ad19b --- /dev/null +++ b/working/music-svg-app-design/concepts/27a-dupre-studios-leather-analog-header-playlist.png diff --git a/working/music-svg-app-design/concepts/27b-dupre-studios-leather-analog-header-radio.png b/working/music-svg-app-design/concepts/27b-dupre-studios-leather-analog-header-radio.png Binary files differnew file mode 100644 index 00000000..c02286a2 --- /dev/null +++ b/working/music-svg-app-design/concepts/27b-dupre-studios-leather-analog-header-radio.png diff --git a/working/music-svg-app-design/concepts/28-dupre-studios-balanced-controls-comparison.png b/working/music-svg-app-design/concepts/28-dupre-studios-balanced-controls-comparison.png Binary files differnew file mode 100644 index 00000000..1e8199f9 --- /dev/null +++ b/working/music-svg-app-design/concepts/28-dupre-studios-balanced-controls-comparison.png diff --git a/working/music-svg-app-design/concepts/28a-dupre-studios-balanced-controls-playlist.png b/working/music-svg-app-design/concepts/28a-dupre-studios-balanced-controls-playlist.png Binary files differnew file mode 100644 index 00000000..ee329bea --- /dev/null +++ b/working/music-svg-app-design/concepts/28a-dupre-studios-balanced-controls-playlist.png diff --git a/working/music-svg-app-design/concepts/28b-dupre-studios-balanced-controls-radio.png b/working/music-svg-app-design/concepts/28b-dupre-studios-balanced-controls-radio.png Binary files differnew file mode 100644 index 00000000..516f555f --- /dev/null +++ b/working/music-svg-app-design/concepts/28b-dupre-studios-balanced-controls-radio.png diff --git a/working/music-svg-app-design/concepts/29a-dupre-studios-brass-seek-playlist.png b/working/music-svg-app-design/concepts/29a-dupre-studios-brass-seek-playlist.png Binary files differnew file mode 100644 index 00000000..20871536 --- /dev/null +++ b/working/music-svg-app-design/concepts/29a-dupre-studios-brass-seek-playlist.png diff --git a/working/music-svg-app-design/concepts/29a-dupre-studios-brass-seek-playlist.xcf b/working/music-svg-app-design/concepts/29a-dupre-studios-brass-seek-playlist.xcf Binary files differnew file mode 100644 index 00000000..9691728b --- /dev/null +++ b/working/music-svg-app-design/concepts/29a-dupre-studios-brass-seek-playlist.xcf diff --git a/working/music-svg-app-design/concepts/30a-dupre-studios-user-refined-playlist.png b/working/music-svg-app-design/concepts/30a-dupre-studios-user-refined-playlist.png Binary files differnew file mode 100644 index 00000000..50dae41b --- /dev/null +++ b/working/music-svg-app-design/concepts/30a-dupre-studios-user-refined-playlist.png diff --git a/working/music-svg-app-design/music-svg-directions.html b/working/music-svg-app-design/music-svg-directions.html new file mode 100644 index 00000000..07b0456e --- /dev/null +++ b/working/music-svg-app-design/music-svg-directions.html @@ -0,0 +1,35 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>Music SVG application — six directions</title> +<style> +:root{--ground:#151311;--panel:#100f0f;--raise:#1a1917;--well:#090a0b;--cream:#f3e7c5;--steel:#969385;--dim:#6f7478;--gold:#e2a038;--goldhi:#ffbe54;--amber:#f0b552;--green:#7fe0a0;--red:#e2543f;--line:#393226;--mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace}*{box-sizing:border-box}body{margin:0;background:radial-gradient(ellipse at 50% -20%,#302818 0,transparent 55%),var(--ground);color:var(--cream);font:14px/1.35 var(--mono)}header{padding:20px max(20px,calc((100vw - 1320px)/2));border-bottom:1px solid var(--line);background:#100f0fdd;position:sticky;top:0;z-index:4;backdrop-filter:blur(12px)}.eyebrow{color:var(--steel);font-size:10px;letter-spacing:.22em;text-transform:uppercase}.head{display:flex;gap:20px;align-items:end;justify-content:space-between;flex-wrap:wrap}.head h1{font-size:22px;color:var(--goldhi);margin:3px 0}.head p{max-width:640px;margin:0;color:var(--steel)}.tabs{display:flex;gap:6px;flex-wrap:wrap}.tabs button,.button{font:inherit;color:var(--steel);background:var(--raise);border:1px solid var(--line);border-radius:5px;padding:7px 9px;cursor:pointer}.tabs button:hover,.tabs button.on,.button:hover{color:var(--panel);background:var(--gold);border-color:var(--goldhi)}main{max-width:1320px;margin:auto;padding:28px 20px 60px}.note{color:var(--steel);margin:0 0 14px}.concept{display:none;min-height:650px}.concept.on{display:block}.face{min-height:640px;padding:20px;border:1px solid #5d471d;border-radius:16px;background:linear-gradient(135deg,#211b12,#0b0b0b 45%,#19150f);box-shadow:inset 0 1px #ffffff13,0 18px 55px #0008;position:relative;overflow:hidden}.face:before,.face:after{content:"";position:absolute;left:16px;right:16px;height:2px;background:linear-gradient(90deg,transparent,var(--gold),var(--goldhi),var(--gold),transparent);opacity:.9}.face:before{top:11px}.face:after{bottom:11px}.label{color:var(--gold);font-size:10px;letter-spacing:.18em;text-transform:uppercase}.well{background:linear-gradient(#060707,#121313);border:1px solid #342b1b;border-radius:8px;box-shadow:inset 0 2px 12px #000;padding:13px}.screen{color:var(--green);text-shadow:0 0 8px #7fe0a077}.title{font:600 clamp(20px,3vw,38px) Georgia,serif;color:var(--cream)}.sub{color:var(--steel)}.gold{color:var(--goldhi)}.lamp{display:inline-block;width:9px;height:9px;border-radius:100%;background:#312818;border:1px solid #6b5223}.lamp.on{background:var(--goldhi);box-shadow:0 0 12px #ffbe54}.lamp.live{background:var(--green);box-shadow:0 0 12px #7fe0a0}.lamp.warn{background:var(--red);box-shadow:0 0 12px #e2543f}.transport{display:flex;gap:8px;align-items:center;justify-content:center}.transport button{width:46px;height:39px;border-radius:6px;border:1px solid #685025;background:linear-gradient(#3a2e1c,#17130d);color:var(--goldhi);font:18px var(--mono);cursor:pointer}.transport button.play{background:linear-gradient(#f0c46c,#8f671f);color:#181109}.transport button:active{transform:translateY(1px);filter:brightness(1.2)}.queue{display:grid;gap:4px}.track{display:grid;grid-template-columns:30px 1fr auto;gap:10px;align-items:center;padding:7px 8px;border-bottom:1px solid #ffffff0b;cursor:pointer}.track:hover,.track.current{background:#e2a03818}.track.current{border-left:3px solid var(--goldhi)}.track .num{color:var(--gold)}.track .meta{color:var(--steel);font-size:11px}.progress{height:7px;background:#28221a;border:1px solid #554321;border-radius:8px;overflow:hidden}.progress i{display:block;height:100%;width:42%;background:linear-gradient(90deg,#8f671f,var(--goldhi),#fff0af);box-shadow:0 0 8px var(--goldhi)}.presets{display:flex;gap:6px;flex-wrap:wrap}.preset{padding:5px 8px;border:1px solid #49391e;border-radius:20px;background:#17130e;color:var(--steel);cursor:pointer}.preset.on{color:var(--panel);background:var(--goldhi)}.knob{width:84px;height:84px;border:8px solid #5c4319;border-radius:50%;background:radial-gradient(circle at 38% 30%,#ffe0a0,#b77b27 45%,#3d2b13 68%);position:relative;box-shadow:0 3px 8px #000}.knob:after{content:"";position:absolute;width:3px;height:27px;background:#1b1308;top:8px;left:38px;border-radius:2px;transform:rotate(32deg);transform-origin:50% 34px}.gauge{width:230px;height:126px;overflow:hidden;position:relative}.gauge svg{width:230px;height:230px;position:absolute;bottom:-104px}.gauge b{position:absolute;bottom:7px;left:0;right:0;text-align:center;color:var(--goldhi);font-size:24px}.svg{width:100%;height:100%}.caption{margin-top:13px;color:var(--steel);font-size:12px}.pinstripe{display:grid;grid-template-columns:minmax(240px,1.1fr) minmax(360px,1.8fr) minmax(240px,1fr);gap:16px;align-items:stretch}.odometer{font-size:clamp(27px,5vw,58px);letter-spacing:.12em;color:#17120a;background:var(--cream);border:3px solid var(--gold);padding:10px 14px;border-radius:6px;box-shadow:inset 0 -5px #b9a988}.dashboard{display:grid;grid-template-columns:1fr minmax(340px,1.45fr) 1fr;gap:18px;align-items:center}.dash-center{text-align:center}.dial{width:min(100%,310px);aspect-ratio:1;margin:auto;border:10px solid #30343a;border-radius:50%;background:radial-gradient(circle,#171611 0 52%,transparent 53%),repeating-conic-gradient(from 225deg,#d59a36 0 1deg,transparent 1deg 7deg);position:relative}.dial:after{content:"";position:absolute;left:50%;bottom:50%;width:3px;height:36%;background:var(--red);transform-origin:bottom;transform:rotate(42deg);box-shadow:0 0 8px #e2543f}.dial .dialtext{position:absolute;inset:0;display:grid;place-content:center;text-align:center;color:var(--goldhi)}.chronometer{display:grid;grid-template-columns:minmax(260px,1fr) minmax(280px,1fr);gap:22px}.chrono{border-radius:50%;aspect-ratio:1;max-width:440px;margin:auto;border:12px double var(--gold);background:repeating-radial-gradient(circle,#15110c 0 5px,#1f1910 6px 7px);position:relative;display:grid;place-content:center;text-align:center}.chrono:before{content:"";position:absolute;inset:13px;border:1px solid #c28b2c;border-radius:50%;background:repeating-conic-gradient(#d69b38 0 1deg,transparent 1deg 6deg)}.chrono>*{position:relative}.time{font-size:clamp(34px,6vw,66px);letter-spacing:.05em;color:var(--cream)}.rack{display:grid;grid-template-columns:190px 1fr 210px;gap:14px}.vu{display:flex;gap:7px;align-items:end;height:110px}.vu i{width:12px;background:linear-gradient(var(--red) 0 18%,var(--goldhi) 18% 50%,var(--green) 50%);border-radius:2px;box-shadow:0 0 6px #7fe0a066}.vu i:nth-child(1){height:28%}.vu i:nth-child(2){height:55%}.vu i:nth-child(3){height:38%}.vu i:nth-child(4){height:76%}.vu i:nth-child(5){height:92%}.vu i:nth-child(6){height:64%}.vu i:nth-child(7){height:48%}.calendar{display:grid;grid-template-columns:1fr 1.35fr 1fr;gap:16px}.apertures{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}.aperture{padding:11px 5px;text-align:center;background:#080706;border:2px solid var(--gold);border-radius:5px;color:var(--cream);font-size:20px;box-shadow:inset 0 3px 10px #000}.radio{display:grid;grid-template-columns:1.1fr 1.5fr;gap:18px}.radar{aspect-ratio:1;max-width:370px;margin:auto;border:2px solid #4b742d;border-radius:50%;background:repeating-radial-gradient(circle,#102012 0 1px,transparent 2px 58px),linear-gradient(135deg,#07100a,#122d16);position:relative;overflow:hidden}.radar:before{content:"";position:absolute;inset:50% 0 0 50%;background:conic-gradient(from 15deg,#7fe0a088,transparent 55deg);transform-origin:0 0;animation:sweep 3s linear infinite}.radar:after{content:"• 104.9\A\A ◉ \A\A •";white-space:pre;position:absolute;inset:30px;color:var(--green);font-size:20px;text-shadow:0 0 8px var(--green)}@keyframes sweep{to{transform:rotate(360deg)}}@media(max-width:900px){.pinstripe,.dashboard,.rack,.calendar,.chronometer,.radio{grid-template-columns:1fr}.face{min-height:auto}.gauge{margin:auto}} +</style> +</head> +<body> +<header><div class="head"><div><div class="eyebrow">Music SVG application · six functional directions</div><h1>Which instrument do you want to live with?</h1><p>Every direction drives the same player state: transport, queue selection, radio preset, playback mode, and volume all respond.</p></div><nav class="tabs" id="tabs"></nav></div></header> +<main><p class="note" id="note"></p><div id="stage"></div></main> +<script> +const state={playing:true,track:0,volume:68,preset:'Groove Salad',modes:{repeat:false,random:false,consume:false}}; +const tracks=[['Groove Salad','SomaFM · 256k','◉'],['Blue in Green','Miles Davis · 5:37','♪'],['Dawn Chorus','Tycho · 4:21','♪'],['Drone Zone','SomaFM · 128k','◉'],['Night Drive','Chromatics · 4:06','♪']]; +const names=['Gold-pinstripe tuner','Touring-car dashboard','Chronometer record deck','Studio rack','Perpetual-calendar salon','Radio navigator']; +const notes=['One machined faceplate: the clearest bridge to Waybar direction 1. A tape-counter queue and a brass volume knob make library work feel deliberate.','A driving instrument cluster: progress becomes speed, queue position becomes odometer, and state becomes a small set of unmistakable tell-tales.','A timepiece that treats playback as elapsed time. Best for focused listening: transport is controlled from the bezel and the queue becomes a program card.','A tactile late-70s receiver: VU meters make audio activity visible, while a rack layout gives playlist and library controls equal status.','A restrained horological receiver: album/stream art sits beside calendar-like status apertures. Best if the music panel should feel like a dressed object, not a machine.','A green-phosphor tuner/navigation station: radio discovery takes center stage, with stations as signals and playlists as routes.']; +function controls(){return `<div class="transport"><button data-action="prev">‹‹</button><button class="play" data-action="play">${state.playing?'Ⅱ':'▶'}</button><button data-action="next">››</button><button data-action="seek">↶</button><button data-action="library">☰</button></div>`} +function queue(){return `<div class="queue">${tracks.map((t,i)=>`<div class="track ${i===state.track?'current':''}" data-track="${i}"><span class="num">${String(i+1).padStart(2,'0')}</span><span>${t[2]} ${t[0]}<br><span class="meta">${t[1]}</span></span><span class="lamp ${i===state.track?'live':''}"></span></div>`).join('')}</div>`} +function modes(){return `<div class="presets"><span class="preset ${state.modes.repeat?'on':''}" data-mode="repeat">repeat</span><span class="preset ${state.modes.random?'on':''}" data-mode="random">random</span><span class="preset ${state.modes.consume?'on':''}" data-mode="consume">consume</span></div>`} +function radioPresets(){return `<div class="presets">${['Groove Salad','Drone Zone','FIP','NTS 1'].map(x=>`<span class="preset ${x===state.preset?'on':''}" data-preset="${x}">${x}</span>`).join('')}</div>`} +function now(){let t=tracks[state.track];return `<div class="label">Now playing <span class="lamp ${state.playing?'live':''}"></span></div><div class="title">${t[0]}</div><div class="sub">${t[1]} · ${state.playing?'playing':'paused'}</div><div class="progress"><i style="width:${state.playing?42:13}%"></i></div>`} +function pinstripe(){return `<section class="concept pinstripe on"><div class="face"><div class="pinstripe"><div class="well"><div class="label">Program counter</div><div class="odometer">0${state.track+1}·24</div><p class="caption">Playlist position is a physical counter. The control bank below opens load, save, edit, and delete flows.</p>${modes()}</div><div class="well">${now()}<div style="height:12px"></div>${controls()}<div style="height:16px"></div><div class="label">Live queue</div>${queue()}</div><div class="well" style="display:grid;place-items:center;align-content:center;gap:12px"><div class="label">Volume</div><div class="knob" data-action="volume"></div><b class="gold">${state.volume}%</b><div class="label">Radio presets</div>${radioPresets()}</div></div><p class="caption">Direction 1 — gold pinstripe tuner console. Use as the base language if the player should visibly belong beside the Waybar odometer console.</p></div></section>`} +function dashboard(){return `<section class="concept dashboard"><div class="face"><div class="dashboard"><div class="well"><div class="label">Source / modes</div>${radioPresets()}<div style="height:18px"></div>${modes()}<div style="height:18px"></div><div class="label">Queue</div>${queue()}</div><div class="dash-center"><div class="dial"><div class="dialtext"><div class="label">elapsed</div><div style="font-size:42px">02:18</div><div class="gold">${state.track+1} / ${tracks.length}</div></div></div><div style="height:12px"></div>${controls()}<p class="caption">Needle position means playback progress. Lamps mean real state: green live, amber engaged, red fault or missing media.</p></div><div class="well"><div class="label">Signal and volume</div><div class="gauge"><svg viewBox="0 0 230 230"><path d="M28 172 A95 95 0 0 1 202 172" fill="none" stroke="#5d451e" stroke-width="16"/><path d="M28 172 A95 95 0 0 1 164 83" fill="none" stroke="#e2a038" stroke-width="10"/><line x1="115" y1="172" x2="153" y2="95" stroke="#ffbe54" stroke-width="4"/></svg><b>${state.volume}<small>%</small></b></div><div class="label">Tell-tales</div><p><span class="lamp live"></span> playing <span class="lamp on"></span> repeat <span class="lamp"></span> queue saved</p></div></div><p class="caption">Direction 2 — touring-car dashboard. Strongest one-glance state model; it turns progress, volume, queue position, and errors into instruments rather than labels.</p></div></section>`} +function chronometer(){return `<section class="concept chronometer"><div class="face"><div class="chronometer"><div class="chrono"><div class="label">${state.playing?'running':'held'} chronometer</div><div class="time">02:18</div><div class="sub">of 05:37</div><div style="margin-top:18px">${controls()}</div></div><div class="well">${now()}<div style="height:16px"></div><div class="label">Program card</div>${queue()}<div style="height:15px"></div><div class="label">Playback modes</div>${modes()}<div style="height:15px"></div><div class="label">Station bank</div>${radioPresets()}</div></div><p class="caption">Direction 3 — chronometer record deck. Playback time is the hero; a physical bezel is a compelling seek surface in the SVG port.</p></div></section>`} +function rack(){return `<section class="concept rack"><div class="face"><div class="rack"><div class="well"><div class="label">Input / source</div>${radioPresets()}<div style="height:15px"></div><div class="label">Program modes</div>${modes()}<div style="height:20px"></div><div class="label">Master</div><div class="knob" data-action="volume"></div><p class="gold">${state.volume}%</p></div><div class="well">${now()}<div style="height:18px"></div><div class="label">Stereo program</div><div class="vu">${Array.from({length:7},()=>'<i></i>').join('')}<span style="width:18px"></span>${Array.from({length:7},()=>'<i></i>').join('')}</div><div class="screen" style="margin:12px 0;padding:8px">L −8.1 dB R −7.8 dB ${state.playing?'SIGNAL':'HOLD'}</div>${controls()}</div><div class="well"><div class="label">Tape library / queue</div>${queue()}</div></div><p class="caption">Direction 4 — studio rack and tape machine. The VU meters are alive only while a track is playing; flashing or red meter states become a useful failure vocabulary.</p></div></section>`} +function calendar(){return `<section class="concept calendar"><div class="face"><div class="calendar"><div class="well"><div class="label">Library cabinet</div>${queue()}<div style="height:12px"></div><div class="label">File operations</div><div class="presets"><span class="preset">load</span><span class="preset">save</span><span class="preset">edit</span><span class="preset">delete</span></div></div><div class="well"><div class="label">Music perpetual calendar</div><div class="apertures"><div class="aperture">JAZZ</div><div class="aperture">SIDE A</div><div class="aperture">LIVE</div></div><div style="height:22px"></div>${now()}<div style="height:18px"></div>${controls()}<div style="height:18px"></div>${modes()}</div><div class="well"><div class="label">Station complications</div>${radioPresets()}<div style="height:18px"></div><div class="label">Volume winding</div><div class="knob" data-action="volume"></div><p class="gold">${state.volume}%</p></div></div><p class="caption">Direction 5 — perpetual-calendar dress. Gold-framed apertures make source, mode, and queue identity feel calm and ceremonial; reserve flashing for actual attention, never decoration.</p></div></section>`} +function navigator(){return `<section class="concept radio"><div class="face"><div class="radio"><div><div class="radar"></div><div class="caption" style="text-align:center">The sweep means an active radio discovery request. A fixed blip means a saved station; a pulsing blip means buffering or reconnecting.</div></div><div class="well"><div class="label">Tuner navigator</div>${now()}<div style="height:16px"></div>${controls()}<div style="height:16px"></div><div class="label">Stations</div>${radioPresets()}<div style="height:16px"></div><div class="label">Route / playlist</div>${queue()}<div style="height:16px"></div>${modes()}</div></div><p class="caption">Direction 6 — mission-control radio navigator. Best for elevating radio search, tag browsing, and saved stations to first-class interaction rather than hiding them behind a command row.</p></div></section>`} +const renders=[pinstripe,dashboard,chronometer,rack,calendar,navigator];let active=Math.max(0,Math.min(renders.length-1,Number(new URLSearchParams(location.search).get('direction'))||0)); +function render(){document.querySelector('#tabs').innerHTML=names.map((n,i)=>`<button class="${i===active?'on':''}" data-tab="${i}">${i+1}. ${n}</button>`).join('');document.querySelector('#stage').innerHTML=renders[active]();document.querySelector('#stage .concept').classList.add('on');document.querySelector('#note').textContent=notes[active];bind()} +function bind(){document.querySelectorAll('[data-tab]').forEach(e=>e.onclick=()=>{active=+e.dataset.tab;render()});document.querySelectorAll('[data-track]').forEach(e=>e.onclick=()=>{state.track=+e.dataset.track;render()});document.querySelectorAll('[data-preset]').forEach(e=>e.onclick=()=>{state.preset=e.dataset.preset;state.track=0;render()});document.querySelectorAll('[data-mode]').forEach(e=>e.onclick=()=>{state.modes[e.dataset.mode]=!state.modes[e.dataset.mode];render()});document.querySelectorAll('[data-action]').forEach(e=>e.onclick=()=>{let a=e.dataset.action;if(a==='play')state.playing=!state.playing;if(a==='next')state.track=(state.track+1)%tracks.length;if(a==='prev')state.track=(state.track+tracks.length-1)%tracks.length;if(a==='volume')state.volume=state.volume>82?35:state.volume+8;render()})}render(); +</script> +</body> +</html> diff --git a/working/music-svg-app-design/prototypes/2026-07-19-music-config-ui-remodel-benchmark.org b/working/music-svg-app-design/prototypes/2026-07-19-music-config-ui-remodel-benchmark.org new file mode 100644 index 00000000..7243ad5f --- /dev/null +++ b/working/music-svg-app-design/prototypes/2026-07-19-music-config-ui-remodel-benchmark.org @@ -0,0 +1,61 @@ +#+TITLE: Music Config UI Remodel Prototype Benchmark +#+DATE: 2026-07-19 + +* Purpose + +Measure the rasterization cost that matters for the proposed Emacs SVG port. +The interactive browser prototype proves behavior, but its DOM timings do not +predict librsvg cost. These trials use =rsvg-convert= 2.62.3 against embedded +raster/SVG fixtures at the intended roughly 958×411 dock size. + +The shell loop launched a fresh =rsvg-convert= process for each sample. Process +startup is therefore included, making these results a conservative bound for an +in-process Emacs/librsvg update. + +* Full instrument trial + +- Fixture: concept 30 embedded as a 3.0 MiB data URI, plus live metadata, + playlist rows, progress, VU needles, and volume segments. +- Output: 958×411. +- Duration: 60 seconds. +- Samples: 448. +- Mean: 134.074 ms. +- Median: 133.616 ms. +- 95th percentile: 139.822 ms. +- Range: 125.105–163.231 ms. +- Verdict: fail. A full receiver rasterization cannot drive ten-Hz VU motion. + +* Lower-deck tile trial + +- Fixture: the opaque lower control deck exported at its 585×226 display size + as JPEG quality 88, embedded as a 52 KiB data URI, plus two dynamic needles + and twenty-one dynamic volume segments. +- Output: 585×226. +- Duration: 60 seconds. +- Samples: 1,677. +- Mean: 35.694 ms. +- Median: 35.513 ms. +- 95th percentile: 38.221 ms. +- Range: 31.719–62.626 ms. +- Verdict: pass. Both the under-50-ms median and under-100-ms p95 budgets pass. + +* Resulting implementation constraint + +The production view uses three aligned image regions from one coordinate +manifest: + +1. Upper player glass, redrawn for track, art, seek, and radio-state changes. +2. Lower control deck, redrawn at meter cadence while visible and playing. +3. Playlist, redrawn for queue, selection, and scroll changes. + +The editable neutral source remains lossless and full resolution. The opaque +runtime deck export is sized for the normal dock and may use a high-quality JPEG +after visual comparison with PNG; it is not the editable source. A wider dock +may select a larger cached export, but V1 never returns to full-instrument +meter-rate rasterization. + +* Related artifacts + +- [[file:2026-07-19-music-config-ui-remodel-prototype-1.html][Functional browser prototype]] +- [[file:../2026-07-19-music-config-ui-remodel-render-fixture.svg][Full-render SVG fixture]] +- [[file:../2026-07-19-music-config-ui-remodel-spec.org][Music Config UI Remodel spec]] diff --git a/working/music-svg-app-design/prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html b/working/music-svg-app-design/prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html new file mode 100644 index 00000000..c05a6947 --- /dev/null +++ b/working/music-svg-app-design/prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html @@ -0,0 +1,1450 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>Dupre Studios Music Config UI Remodel</title> +<style> + :root { + --black: #050605; + --glass: #090a09; + --soft-white: #e9e2d3; + --dim-white: #a9a59c; + --amber: #f2a900; + --amber-hot: #ffc34f; + --green: #9dda59; + --red: #a73a29; + --brass: #c9a574; + --coffee: #b69c7d; + --ink: #201810; + --mono: "Berkeley Mono", "BerkeleyMono Nerd Font", ui-monospace, monospace; + } + + * { box-sizing: border-box; } + + [hidden] { display: none !important; } + + html, body { min-height: 100%; } + + body { + margin: 0; + color: var(--soft-white); + background: + radial-gradient(circle at 50% -20%, #2c261e 0, transparent 42%), + linear-gradient(#0e0e0d, #050505 70%); + font-family: var(--mono); + } + + button, input { font: inherit; } + + .workbench { + width: min(100%, 1960px); + margin: 0 auto; + padding: 16px 20px 28px; + } + + .prototype-bar { + min-height: 48px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + margin-bottom: 12px; + color: #bbb3a3; + font-size: 12px; + letter-spacing: .04em; + } + + .prototype-bar h1 { + margin: 0 0 3px; + color: #e8d6b7; + font-size: 14px; + font-weight: 500; + letter-spacing: .13em; + text-transform: uppercase; + } + + .prototype-bar p { margin: 0; } + + .fixture-switch, .bench-controls { + display: flex; + align-items: center; + gap: 7px; + flex-wrap: wrap; + justify-content: flex-end; + } + + .lab-button { + min-height: 31px; + padding: 5px 10px; + color: #bbb3a3; + border: 1px solid #554b3e; + border-radius: 3px; + background: linear-gradient(#26231f, #141311); + cursor: pointer; + } + + .lab-button:hover, .lab-button[aria-pressed="true"] { + color: #17130e; + border-color: #b89765; + background: linear-gradient(#e0c292, #a98857); + } + + .bench-readout { + min-width: 242px; + color: #9b9487; + font-variant-numeric: tabular-nums; + text-align: right; + } + + .receiver-wrap { + width: 100%; + overflow: hidden; + border-radius: 0 0 12px 12px; + box-shadow: 0 28px 60px #000c; + } + + .receiver { + position: relative; + width: 100%; + aspect-ratio: 1916 / 821; + overflow: hidden; + background: #060606 url("../concepts/30a-dupre-studios-user-refined-playlist.png") center / 100% 100% no-repeat; + user-select: none; + touch-action: none; + } + + .dynamic-svg, .hit-layer { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + } + + .dynamic-svg { pointer-events: none; } + + .upper-live { + position: absolute; + left: 31.55%; + top: 11.6%; + width: 27.9%; + height: 31.4%; + overflow: hidden; + background: + radial-gradient(ellipse at 36% 10%, #25242140, transparent 46%), + linear-gradient(100deg, #090a09 0, #060706 72%, #080908 100%); + box-shadow: inset 0 0 34px #000; + } + + .art { + position: absolute; + left: 1.4%; + top: 1.5%; + width: 28%; + height: 69%; + overflow: hidden; + border: 1px solid #292724; + border-radius: 2px; + background: #171716; + box-shadow: 0 5px 15px #000b; + } + + .art img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + } + + .album-crop { + width: 100%; + height: 100%; + background-image: url("../concepts/30a-dupre-studios-user-refined-playlist.png"); + background-size: 1312.33% 475.2%; + background-position: 34.42% 20.9%; + } + + .metadata { + position: absolute; + left: 33.1%; + right: 1.5%; + top: 0; + height: 72%; + display: grid; + align-content: start; + gap: 4.6%; + padding-top: 1.2%; + color: var(--soft-white); + font-size: clamp(7px, 1.04vw, 20px); + line-height: 1.07; + letter-spacing: .08em; + text-shadow: 0 0 6px #fff2; + text-transform: uppercase; + } + + .metadata-line { + min-width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .metadata-line.dim { color: #d5cfc2; } + + .on-air-under-art { + position: absolute; + left: 1.6%; + top: 73.5%; + width: 27.5%; + color: #e9543f; + font-size: clamp(8px, 1vw, 18px); + letter-spacing: .18em; + text-align: center; + text-shadow: 0 0 9px #e9543f99; + } + + .seek-line { + position: absolute; + left: 1.4%; + right: 1.5%; + bottom: 2.5%; + height: 20%; + display: grid; + grid-template-columns: 12% 1fr 12%; + align-items: center; + gap: 3%; + color: #d2cabd; + font-size: clamp(8px, .94vw, 18px); + font-variant-numeric: tabular-nums; + } + + .seek-time:last-child { text-align: right; } + + .seek-track { + position: relative; + height: 12px; + cursor: ew-resize; + } + + .seek-track::before { + content: ""; + position: absolute; + left: 0; + right: 0; + top: 5px; + height: 2px; + background: #373735; + box-shadow: inset 0 1px #000; + } + + .seek-fill { + position: absolute; + left: 0; + top: 5px; + width: calc(var(--progress) * 1%); + height: 2px; + background: var(--amber); + box-shadow: 0 0 4px #f2a90088; + } + + .seek-thumb { + position: absolute; + top: 50%; + left: calc(var(--progress) * 1%); + width: 12px; + height: 21px; + border: 1px solid #ead0a4; + border-radius: 3px; + background: linear-gradient(90deg, #6f4c2d, #caa16a 22%, #f0d3a0 48%, #ad7f4a 74%, #654429); + box-shadow: 0 2px 4px #000d, inset 0 1px #fff4, inset 0 -1px #50351f; + transform: translate(-50%, -50%); + } + + .seek-thumb::after { + content: ""; + position: absolute; + left: 50%; + top: 3px; + bottom: 3px; + width: 1px; + background: #5f4026aa; + box-shadow: 1px 0 #f2d7aa66; + } + + .playlist-header { + position: absolute; + left: 61.16%; + top: 6.35%; + width: 36%; + height: 7.8%; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 2.4%; + padding: 0 2.1%; + overflow: hidden; + color: #1f1811; + background: + radial-gradient(ellipse 10% 100% at 5% -3%, #fff1d7 0, #eac184aa 35%, transparent 76%), + radial-gradient(ellipse 10% 100% at 35% -3%, #fff1d7 0, #eac184aa 35%, transparent 76%), + radial-gradient(ellipse 10% 100% at 65% -3%, #fff1d7 0, #eac184aa 35%, transparent 76%), + radial-gradient(ellipse 10% 100% at 95% -3%, #fff1d7 0, #eac184aa 35%, transparent 76%), + linear-gradient(90deg, #d5b78c22, transparent 25% 75%, #5c3c2233), + linear-gradient(180deg, #92795e, #877057); + box-shadow: + inset 0 1px #f7dfb6, + inset 0 5px 14px #fff2, + inset 0 -8px 15px #5a3d2433, + inset 0 0 0 1px #73583b66; + font-size: clamp(8px, 1.04vw, 20px); + letter-spacing: .075em; + text-transform: uppercase; + } + + .header-name { + min-width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .playlist-live { + position: absolute; + left: 61.05%; + top: 14.25%; + width: 36.12%; + height: 66.15%; + overflow: hidden; + background: + radial-gradient(ellipse at 48% 0, #20211e22, transparent 55%), + linear-gradient(90deg, #070807, #090a09 65%, #050605); + box-shadow: inset 0 0 26px #000; + } + + .playlist-scroll { + position: absolute; + inset: 0 22px 0 0; + overflow-y: scroll; + scrollbar-width: none; + overscroll-behavior: contain; + } + + .playlist-scroll::-webkit-scrollbar { display: none; } + + .playlist-rows { min-height: 100%; } + + .playlist-row { + height: var(--row-height, 37px); + min-height: var(--row-height, 37px); + display: grid; + grid-template-columns: 7.5% minmax(0, 1fr) 12%; + align-items: center; + gap: 1.5%; + padding: 0 1.2% 0 2.6%; + color: #dad6cd; + border-bottom: 1px solid #ffffff06; + font-size: clamp(7px, .91vw, 17px); + letter-spacing: .045em; + text-transform: uppercase; + cursor: pointer; + } + + .playlist-row:hover { background: #f2a9000b; color: #fffaf0; } + + .playlist-row.selected { + color: var(--amber); + background: linear-gradient(90deg, #f2a90012, transparent 72%); + text-shadow: 0 0 7px #f2a90044; + } + + .row-number { position: relative; font-variant-numeric: tabular-nums; } + + .playlist-row.selected .row-number::before { + content: "▶"; + position: absolute; + right: calc(100% + 4px); + font-size: .78em; + } + + .row-name { + min-width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .row-meta { text-align: right; font-variant-numeric: tabular-nums; } + + .scroll-rail { + position: absolute; + top: 1.2%; + right: 5px; + bottom: 1.2%; + width: 9px; + border: 1px solid #554b3d; + border-radius: 5px; + background: #191815; + box-shadow: inset 0 1px 3px #000; + } + + .scroll-thumb { + position: absolute; + left: -1px; + top: 0; + width: 9px; + min-height: 38px; + border: 1px solid #aa9574; + border-radius: 5px; + background: linear-gradient(90deg, #7b6a53, #bca887 45%, #76634b); + box-shadow: 0 0 4px #000; + cursor: ns-resize; + } + + .empty-row { + height: var(--row-height, 37px); + border-bottom: 1px solid #ffffff05; + } + + .transport-bank { + position: absolute; + left: 5.95%; + top: 71.7%; + width: 24.5%; + height: 7.45%; + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 5.4%; + } + + .transport-button { + position: relative; + border: 1px solid #1d1b18; + border-radius: 4px; + color: #b6a687; + background: + linear-gradient(180deg, #292927 0 7%, #171716 15% 72%, #090a09 100%); + box-shadow: + inset 0 1px #5b5953, + inset 0 -3px #050505, + 0 5px 5px #0009; + font-size: clamp(10px, 1.4vw, 25px); + cursor: pointer; + transform: translateY(0); + } + + .transport-button:hover { color: #e9ddc6; filter: brightness(1.12); } + + .transport-button:active, .transport-button.pressed { + transform: translateY(3px); + box-shadow: inset 0 2px 4px #000, 0 1px 2px #000b; + } + + .transport-button.playing { + color: #bdd27c; + text-shadow: 0 0 2px #a7c26c, 0 0 4px #70883e99; + transform: translateY(3px); + box-shadow: inset 0 2px 5px #000, 0 1px 2px #000b; + } + + .mode-button, + .radio-button { + position: absolute; + width: 2.05%; + aspect-ratio: 1; + padding: 0; + border: 1px solid #6e5436; + border-radius: 50%; + color: #2a2118; + background: + radial-gradient(circle at 34% 25%, #fff0c5 0 2.5%, transparent 5%), + repeating-radial-gradient(circle at 50% 50%, #fff3d00b 0 1px, #4b2e160b 1px 2px), + radial-gradient(circle at 46% 42%, #e8cb94 0, #c79a60 52%, #8b6038 82%, #d3aa70 100%); + box-shadow: + 0 3px 3px #0009, + inset 0 1px #f2d39e, + 0 0 0 3px #342719, + 0 0 0 4px #d0ad78; + cursor: pointer; + transform: translateY(0); + } + + .mode-button::after { + content: ""; + position: absolute; + inset: -4px; + border: 1px solid transparent; + border-radius: 50%; + } + + .mode-button[aria-pressed="true"]::after { + border-color: #829b55; + box-shadow: 0 0 2px #819a55aa, inset 0 0 2px #71864988; + } + + .mode-button[aria-pressed="true"], .mode-button:active { + transform: translateY(2px); + box-shadow: + 0 1px 1px #0009, + inset 0 2px 3px #3c291777, + 0 0 0 3px #2c2117, + 0 0 0 4px #bb9766; + } + + .radio-button { + box-shadow: + 0 3px 3px #0009, + inset 0 1px #f2d39e, + 0 0 0 3px #342719, + 0 0 0 4px #d0ad78; + } + + .radio-button:active { + transform: translateY(2px); + box-shadow: + 0 1px 1px #0009, + inset 0 2px 3px #3c291777, + 0 0 0 3px #2c2117, + 0 0 0 4px #bb9766; + } + + .mode-repeat { left: 34.68%; top: 52.75%; } + .mode-single { left: 39.18%; top: 52.75%; } + .mode-random { left: 34.68%; top: 61.25%; } + .mode-consume { left: 39.18%; top: 61.25%; } + .radio-name { left: 34.68%; top: 75.65%; } + .radio-tags { left: 39.18%; top: 75.65%; } + + .volume-hit { + position: absolute; + left: 43.2%; + top: 49.6%; + width: 14.6%; + height: 34%; + border-radius: 50%; + cursor: grab; + } + + .volume-hit:active { cursor: grabbing; } + + .playlist-actions { + position: absolute; + left: 61.13%; + top: 81.65%; + width: 35.45%; + height: 7.45%; + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: .65%; + } + + .playlist-action { + border: 1px solid #302b23; + border-radius: 2px; + color: #ccc5b8; + background: linear-gradient(#1d1c19, #090a09); + box-shadow: inset 0 1px #555147, 0 3px 5px #0007; + font-size: clamp(8px, 1vw, 19px); + letter-spacing: .05em; + cursor: pointer; + } + + .playlist-action[data-action="add"], + .playlist-action[data-action="new"] { + background: linear-gradient(#29271d, #11120d); + } + + .playlist-action[data-action="delete"] { + color: #ead3cc; + border-color: #572c24; + background: linear-gradient(#552019, #2e0f0b); + } + + .playlist-action:hover { filter: brightness(1.2); } + + .playlist-action:active { transform: translateY(2px); box-shadow: inset 0 2px 5px #000; } + + .status-toast { + position: absolute; + left: 50%; + bottom: 2.7%; + z-index: 8; + max-width: 52%; + padding: .55% 1%; + color: #d7ccb8; + border: 1px solid #6d5b42; + border-radius: 3px; + background: #090907ed; + box-shadow: 0 5px 14px #000c; + font-size: clamp(7px, .78vw, 15px); + letter-spacing: .04em; + opacity: 0; + pointer-events: none; + transform: translate(-50%, 8px); + transition: opacity .15s, transform .15s; + } + + .status-toast.show { opacity: 1; transform: translate(-50%, 0); } + + dialog { + width: min(430px, calc(100vw - 40px)); + color: #e6dccb; + border: 1px solid #9b7651; + border-radius: 6px; + background: linear-gradient(#1d1b18, #0b0b0a); + box-shadow: 0 24px 70px #000; + font-family: var(--mono); + } + + dialog::backdrop { background: #000b; } + dialog h2 { color: #e0be87; font-size: 16px; font-weight: 500; } + dialog p { color: #bdb5a8; font-size: 13px; line-height: 1.45; } + dialog menu { display: flex; justify-content: flex-end; gap: 8px; padding: 0; } + + .delete-confirm { + color: #f0dad3; + border-color: #7c3327; + background: #592018; + } + + .inspection-note { + margin: 13px 2px 0; + display: flex; + justify-content: space-between; + gap: 18px; + color: #7f796f; + font-size: 11px; + line-height: 1.5; + } + + .inspection-note span:last-child { text-align: right; } + + @media (max-width: 980px) { + .workbench { padding: 10px 8px 20px; } + .prototype-bar { align-items: flex-start; flex-direction: column; } + .fixture-switch, .bench-controls { justify-content: flex-start; } + .bench-readout { text-align: left; } + } +</style> +</head> +<body> +<main class="workbench"> + <header class="prototype-bar"> + <div> + <h1>Music Config UI Remodel · Functional Prototype 1</h1> + <p>Drive the receiver. The controls outside it only switch fixtures and run the review benchmark.</p> + </div> + <div class="fixture-switch" aria-label="Prototype fixtures"> + <button class="lab-button" id="localFixture" aria-pressed="true">Local playlist</button> + <button class="lab-button" id="radioFixture" aria-pressed="false">Radio station</button> + <button class="lab-button" id="restoreFixture">Restore fixture</button> + </div> + <div class="bench-controls"> + <button class="lab-button" id="benchmarkButton">Run 60s benchmark</button> + <span class="bench-readout" id="benchmarkReadout">Dynamic updates: awaiting benchmark</span> + </div> + </header> + + <section class="receiver-wrap" aria-label="Dupre Studios receiver prototype"> + <div class="receiver" id="receiver"> + <div class="upper-live" id="upperLive"> + <div class="art" id="art"><div class="album-crop" aria-label="69 Love Songs album art"></div></div> + <div class="metadata" id="metadata"></div> + <div class="on-air-under-art" id="onAirUnderArt" hidden>ON AIR</div> + <div class="seek-line" id="seekLine"> + <span class="seek-time" id="elapsed">01:30</span> + <div class="seek-track" id="seekTrack" role="slider" tabindex="0" aria-label="Track position" aria-valuemin="0" aria-valuemax="100" aria-valuenow="50"> + <div class="seek-fill"></div> + <div class="seek-thumb"></div> + </div> + <span class="seek-time" id="duration">02:59</span> + </div> + </div> + + <div class="playlist-header" id="playlistHeader"> + <span class="header-name" id="headerName">69 LOVE SONGS — COMPLETE</span> + <span id="headerCount">69 TRACKS</span> + </div> + + <div class="playlist-live"> + <div class="playlist-scroll" id="playlistScroll" tabindex="0" aria-label="Playlist rows"> + <div class="playlist-rows" id="playlistRows"></div> + </div> + <div class="scroll-rail" id="scrollRail" hidden><div class="scroll-thumb" id="scrollThumb"></div></div> + </div> + + <svg class="dynamic-svg" viewBox="0 0 1916 821" aria-hidden="true"> + <defs> + <radialGradient id="meterGlow" cx="50%" cy="100%" r="65%"> + <stop offset="0" stop-color="#ffc55b" stop-opacity=".75"/> + <stop offset=".32" stop-color="#8c5b16" stop-opacity=".18"/> + <stop offset="1" stop-color="#000" stop-opacity="0"/> + </radialGradient> + <linearGradient id="meterFrame" x1="0" x2="1"> + <stop offset="0" stop-color="#5b4325"/> + <stop offset=".18" stop-color="#e2c08a"/> + <stop offset=".5" stop-color="#7c5a32"/> + <stop offset=".82" stop-color="#ddba82"/> + <stop offset="1" stop-color="#4a321c"/> + </linearGradient> + <linearGradient id="faceplateMask" x1="0" y1="0" x2="1" y2="1"> + <stop offset="0" stop-color="#c3a477"/> + <stop offset=".52" stop-color="#b9996d"/> + <stop offset="1" stop-color="#c5a77b"/> + </linearGradient> + <linearGradient id="champagneFace" x1="0" y1="0" x2="0" y2="1"> + <stop offset="0" stop-color="#c8b89f"/> + <stop offset=".18" stop-color="#b9a489"/> + <stop offset=".56" stop-color="#b19a7c"/> + <stop offset="1" stop-color="#a68e71"/> + </linearGradient> + <linearGradient id="glassLip" x1="0" y1="0" x2="0" y2="1"> + <stop offset="0" stop-color="#050606" stop-opacity=".96"/> + <stop offset=".45" stop-color="#111311"/> + <stop offset=".76" stop-color="#030404"/> + <stop offset="1" stop-color="#2a2118"/> + </linearGradient> + <pattern id="microBrush" width="5" height="4" patternUnits="userSpaceOnUse"> + <path d="M0 .5 H5" stroke="#fff5df" stroke-opacity=".1" stroke-width=".55"/> + <path d="M0 3.5 H5" stroke="#57452f" stroke-opacity=".08" stroke-width=".45"/> + </pattern> + <clipPath id="volumeKnobClip"> + <circle cx="972" cy="551" r="98"/> + </clipPath> + <radialGradient id="volumeLamp" cx="36%" cy="28%" r="70%"> + <stop offset="0" stop-color="#fff8d7"/> + <stop offset=".24" stop-color="#ffd27d"/> + <stop offset=".72" stop-color="#f1a02e"/> + <stop offset="1" stop-color="#b96513"/> + </radialGradient> + </defs> + <g id="faceplateSurface"> + <rect x="69" y="366" width="1078" height="31" rx="19" fill="url(#glassLip)"/> + <path d="M88 369 H1128 Q1143 369 1147 385" fill="none" stroke="#353735" stroke-opacity=".8" stroke-width="2"/> + <rect x="69" y="379" width="1078" height="367" rx="19" fill="url(#champagneFace)"/> + <rect x="69" y="379" width="1078" height="367" rx="19" fill="url(#microBrush)"/> + <path d="M88 379 H1128 Q1142 379 1147 395" fill="none" stroke="#f0d09c" stroke-width="2.2"/> + <rect x="73" y="383" width="1070" height="359" rx="16" fill="none" stroke="#d2b584" stroke-width="1.5"/> + <rect x="77" y="387" width="1062" height="351" rx="14" fill="none" stroke="#715438" stroke-opacity=".72" stroke-width="1.4"/> + <line x1="641" y1="578" x2="817" y2="578" stroke="#705438" stroke-width="1.2"/> + <g fill="#261d16" font-family="Berkeley Mono, monospace" text-anchor="middle" letter-spacing=".5"> + <text x="729" y="422" font-size="17">PLAY MODES</text> + <text x="684" y="492" font-size="15">REPEAT</text> + <text x="770" y="492" font-size="15">SINGLE</text> + <text x="684" y="563" font-size="15">RANDOM</text> + <text x="770" y="563" font-size="15">CONSUME</text> + <text x="729" y="608" font-size="16">RADIO SEARCH</text> + <text x="684" y="686" font-size="15">NAME</text> + <text x="770" y="686" font-size="15">TAGS</text> + <text x="162" y="690" font-size="16">PREV</text> + <text x="285" y="690" font-size="16">PLAY/PAUSE</text> + <text x="408" y="690" font-size="16">STOP</text> + <text x="531" y="690" font-size="16">NEXT</text> + <text x="972" y="691" font-size="17">VOLUME</text> + </g> + <image href="../concepts/30a-dupre-studios-user-refined-playlist.png" + x="0" y="0" width="1916" height="821" clip-path="url(#volumeKnobClip)"/> + </g> + <g id="meters"></g> + <g id="volumeMask"></g> + <g id="volumeSegments"></g> + </svg> + + <div class="transport-bank" aria-label="Transport controls"> + <button class="transport-button" data-transport="prev" aria-label="Previous">|◀</button> + <button class="transport-button" data-transport="play" aria-label="Play or pause">▶Ⅱ</button> + <button class="transport-button" data-transport="stop" aria-label="Stop">■</button> + <button class="transport-button" data-transport="next" aria-label="Next">▶|</button> + </div> + + <button class="mode-button mode-repeat" data-mode="repeat" aria-label="Repeat playlist" aria-pressed="false"></button> + <button class="mode-button mode-single" data-mode="single" aria-label="Repeat one track" aria-pressed="false"></button> + <button class="mode-button mode-random" data-mode="random" aria-label="Random playback" aria-pressed="false"></button> + <button class="mode-button mode-consume" data-mode="consume" aria-label="Consume tracks" aria-pressed="false"></button> + <button class="radio-button radio-name" data-radio="name" aria-label="Search radio stations by name"></button> + <button class="radio-button radio-tags" data-radio="tags" aria-label="Search radio stations by tag"></button> + <div class="volume-hit" id="volumeHit" role="slider" tabindex="0" aria-label="Player volume" aria-valuemin="0" aria-valuemax="100" aria-valuenow="68"></div> + + <div class="playlist-actions" aria-label="Playlist actions"> + <button class="playlist-action" data-action="add">ADD</button> + <button class="playlist-action" data-action="new">NEW</button> + <button class="playlist-action" data-action="load">LOAD</button> + <button class="playlist-action" data-action="save">SAVE</button> + <button class="playlist-action" data-action="delete">DELETE</button> + </div> + + <div class="status-toast" id="statusToast" role="status" aria-live="polite"></div> + </div> + </section> + + <div class="inspection-note"> + <span>Keyboard: Space play/pause · arrows select · Page Up/Down browse · Home/End · R/S/X/C modes. Prototype VU motion is synthetic; production reads mpv RMS.</span> + <span id="interactionStatus">Local fixture · playing · volume 68</span> + </div> +</main> + +<dialog id="deleteDialog"> + <form method="dialog"> + <h2>Delete this playlist?</h2> + <p id="deletePrompt">This removes the saved playlist. Playback stops and the receiver returns to an untitled empty queue.</p> + <menu> + <button class="lab-button" value="cancel">Cancel</button> + <button class="lab-button delete-confirm" value="confirm">Delete playlist</button> + </menu> + </form> +</dialog> + +<script> +(() => { + "use strict"; + + const BASE_ROW_HEIGHT = 37; + const VISIBLE_ROWS = 14; + const localSeed = [ + ["Absolutely Cuckoo", "The Magnetic Fields", "02:02"], + ["I Don’t Believe in the Sun", "The Magnetic Fields", "02:50"], + ["All My Little Words", "The Magnetic Fields", "02:03"], + ["A Chicken With Its Head Cut Off — Remastered Anniversary Edition", "The Magnetic Fields and the Long-Lost Orchestra", "02:59"], + ["(I’m) Dazota", "The Magnetic Fields", "02:38"], + ["I Don’t Want to Get Over You", "The Magnetic Fields", "02:50"], + ["Come Back From San Francisco", "The Magnetic Fields", "02:37"], + ["The Luckiest Guy on the Lower East Side", "The Magnetic Fields", "01:32"], + ["Let’s Pretend We’re Bunny Rabbits", "The Magnetic Fields", "02:10"], + ["The Cactus Where Your Heart Should Be", "The Magnetic Fields", "02:38"], + ["I Think I Need a New Heart", "The Magnetic Fields", "02:58"], + ["The Book of Love", "The Magnetic Fields", "02:56"], + ["Fido, Your Leash Is Too Long", "The Magnetic Fields", "02:13"], + ["How Fucking Romantic", "The Magnetic Fields", "00:58"], + ["The One You Really Love", "The Magnetic Fields", "02:53"], + ["Punk Love", "The Magnetic Fields", "00:58"], + ["Parades Go By", "The Magnetic Fields", "02:56"], + ["Boa Constrictor", "The Magnetic Fields", "00:58"], + ["A Pretty Girl Is Like…", "The Magnetic Fields", "01:50"], + ["My Sentimental Melody", "The Magnetic Fields", "03:07"] + ]; + + const fillerTitles = [ + "Nothing Matters When We’re Dancing", "Sweet-Lovin’ Man", "The Things We Did and Didn’t Do", + "Roses", "Love Is Like Jazz", "When My Boy Walks Down the Street", "Time Enough for Rocking", + "Very Funny", "Grand Canyon", "No One Will Ever Love You", "If You Don’t Cry", + "You’re My Only Home", "Washington, D.C.", "Long-Forgotten Fairytale" + ]; + + const radioSeed = [ + ["Groove Salad", "SomaFM · Ambient / Downtempo", "ON AIR"], + ["Drone Zone", "SomaFM · Atmospheric Textures", "256k"], + ["Secret Agent", "SomaFM · Cinematic", "256k"], + ["Illinois Street Lounge", "SomaFM · Lounge", "256k"], + ["Space Station Soma", "SomaFM · Space Music", "256k"], + ["Left Coast 70s", "SomaFM · Mellow Rock", "256k"], + ["The Trip", "SomaFM · Progressive House", "256k"], + ["Underground 80s", "SomaFM · Synthpop", "256k"], + ["Deep Space One", "SomaFM · Ambient", "256k"], + ["Black Rock FM", "SomaFM · Eclectic", "256k"], + ["Bossa Beyond", "SomaFM · Brazilian", "256k"], + ["Seven Inch Soul", "SomaFM · Vintage Soul", "256k"], + ["Suburbs of Goa", "SomaFM · South Asian", "256k"], + ["Boot Liquor", "SomaFM · Americana", "256k"], + ["Heavyweight Reggae", "SomaFM · Roots Reggae", "256k"], + ["Lush", "SomaFM · Female Vocals", "256k"], + ["Beat Blender", "SomaFM · Electronic", "256k"], + ["Digitalis", "SomaFM · Indie Electronic", "256k"] + ]; + + const makeLocalTracks = () => Array.from({length: 69}, (_, index) => { + const base = localSeed[index] || [ + fillerTitles[index % fillerTitles.length], + index % 7 === 0 ? "The Magnetic Fields with an Improbably Long Guest Credit" : "The Magnetic Fields", + `${String(1 + (index % 3)).padStart(2, "0")}:${String((17 * index) % 60).padStart(2, "0")}` + ]; + return {id: `local-${index}`, title: base[0], artist: base[1], meta: base[2]}; + }); + + const makeRadioTracks = () => radioSeed.map((row, index) => ({ + id: `radio-${index}`, title: row[0], artist: row[1], meta: row[2] + })); + + const fixtures = { + local: { + name: "69 Love Songs — The Complete Three-Volume Collection", + kind: "PLAYLIST", + tracks: makeLocalTracks, + selected: 3, + position: 90, + duration: 179, + metadata: [ + "A Chicken With Its Head Cut Off — Remastered Anniversary Edition", + "The Magnetic Fields and the Long-Lost Orchestra", + "69 Love Songs: The Complete Three-Volume Collection", + "Merge Records", + "1999" + ] + }, + radio: { + name: "Name Search · Ambient", + kind: "RADIO", + tracks: makeRadioTracks, + selected: 0, + position: 0, + duration: 0, + metadata: ["Groove Salad", "SomaFM", "Ambient / Downtempo", "256 kbps"] + } + }; + + const state = { + fixture: "local", + playlistName: fixtures.local.name, + tracks: fixtures.local.tracks(), + selected: fixtures.local.selected, + position: fixtures.local.position, + duration: fixtures.local.duration, + playing: true, + volume: 68, + modes: {repeat: false, single: false, random: false, consume: false}, + search: null, + vu: {left: -20, right: -20}, + benchmark: null + }; + + const $ = selector => document.querySelector(selector); + const $$ = selector => [...document.querySelectorAll(selector)]; + const receiver = $("#receiver"); + const rows = $("#playlistRows"); + const scroll = $("#playlistScroll"); + const rail = $("#scrollRail"); + const thumb = $("#scrollThumb"); + const seekTrack = $("#seekTrack"); + const volumeHit = $("#volumeHit"); + const toast = $("#statusToast"); + const deleteDialog = $("#deleteDialog"); + let toastTimer = null; + let seekDragging = false; + let volumeDragging = false; + let thumbDragging = false; + let thumbGrabOffset = 0; + let lastMeterTime = performance.now(); + + const clamp = (value, min, max) => Math.max(min, Math.min(max, value)); + const formatTime = seconds => { + const value = Math.max(0, Math.round(seconds)); + return `${String(Math.floor(value / 60)).padStart(2, "0")}:${String(value % 60).padStart(2, "0")}`; + }; + + function announce(message) { + clearTimeout(toastTimer); + toast.textContent = message; + toast.classList.add("show"); + toastTimer = setTimeout(() => toast.classList.remove("show"), 1800); + updateInspection(message); + } + + function currentTrack() { return state.tracks[state.selected] || null; } + + function switchFixture(name, message = null) { + const source = fixtures[name]; + state.fixture = name; + state.playlistName = source.name; + state.tracks = source.tracks(); + state.selected = source.selected; + state.position = source.position; + state.duration = source.duration; + state.playing = true; + state.search = null; + scroll.scrollTop = 0; + renderAll(); + announce(message || `${name === "local" ? "Local playlist" : "Radio station"} fixture loaded`); + } + + function renderInfo() { + const source = fixtures[state.fixture]; + const track = currentTrack(); + const metadata = track ? (state.fixture === "local" ? [ + track.title, + track.artist, + source.metadata[2], + source.metadata[3], + source.metadata[4] + ] : [track.title, "SomaFM", track.artist.replace(/^SomaFM · /, ""), "256 kbps"]) : []; + + $("#metadata").innerHTML = metadata.filter(Boolean).map((line, index) => + `<div class="metadata-line ${index ? "dim" : ""}" title="${escapeHtml(line)}">${escapeHtml(line)}</div>` + ).join(""); + + $("#art").innerHTML = state.fixture === "radio" + ? '<img src="../../../assets/vinyl-placeholder.svg" alt="Radio station artwork unavailable; vinyl placeholder">' + : '<div class="album-crop" role="img" aria-label="69 Love Songs album art"></div>'; + + const onAir = state.fixture === "radio" && Boolean(track); + $("#onAirUnderArt").hidden = !onAir; + $("#seekLine").hidden = onAir || !track; + if (track && !onAir) updateSeekVisual(); + } + + function escapeHtml(value) { + return String(value).replace(/[&<>"']/g, character => ({ + "&": "&", "<": "<", ">": ">", '"': """, "'": "'" + })[character]); + } + + function renderHeader() { + $("#headerName").textContent = state.playlistName || "UNTITLED"; + $("#headerName").title = state.playlistName || "UNTITLED"; + const noun = state.fixture === "radio" ? (state.tracks.length === 1 ? "STATION" : "STATIONS") : (state.tracks.length === 1 ? "TRACK" : "TRACKS"); + $("#headerCount").textContent = `${state.tracks.length} ${noun}`; + } + + function renderRows() { + const rowHeight = scroll.clientHeight / VISIBLE_ROWS; + rows.style.setProperty("--row-height", `${rowHeight}px`); + if (!state.tracks.length) { + rows.innerHTML = Array.from({length: VISIBLE_ROWS}, () => '<div class="empty-row"></div>').join(""); + rows.style.height = `${scroll.clientHeight}px`; + rail.hidden = true; + return; + } + rows.style.height = `${rowHeight * Math.max(VISIBLE_ROWS, state.tracks.length)}px`; + rows.innerHTML = state.tracks.map((track, index) => ` + <div class="playlist-row ${index === state.selected ? "selected" : ""}" data-row="${index}" title="${escapeHtml(`${track.title} — ${track.artist}`)}"> + <span class="row-number">${String(index + 1).padStart(2, "0")}</span> + <span class="row-name">${escapeHtml(track.title)} — ${escapeHtml(track.artist)}</span> + <span class="row-meta">${state.fixture === "radio" && index === state.selected && state.playing ? "ON AIR" : escapeHtml(track.meta)}</span> + </div>`).join(""); + $$("[data-row]").forEach(row => row.addEventListener("click", () => selectTrack(Number(row.dataset.row), true))); + requestAnimationFrame(updateScrollbar); + } + + function renderControls() { + const hasTrack = Boolean(currentTrack()); + const play = $('[data-transport="play"]'); + play.classList.toggle("playing", state.playing && hasTrack); + play.disabled = !hasTrack; + $('[data-transport="prev"]').disabled = !hasTrack; + $('[data-transport="next"]').disabled = !hasTrack; + $('[data-transport="stop"]').disabled = !hasTrack; + $$("[data-mode]").forEach(button => button.setAttribute("aria-pressed", String(state.modes[button.dataset.mode]))); + $("#localFixture").setAttribute("aria-pressed", String(state.fixture === "local")); + $("#radioFixture").setAttribute("aria-pressed", String(state.fixture === "radio")); + volumeHit.setAttribute("aria-valuenow", String(state.volume)); + updateVolumeSegments(); + updateInspection(); + } + + function renderAll() { + const started = performance.now(); + renderInfo(); + renderHeader(); + renderRows(); + renderControls(); + recordRender(performance.now() - started); + } + + function updateSeekVisual() { + const percentage = state.duration ? clamp((state.position / state.duration) * 100, 0, 100) : 0; + seekTrack.style.setProperty("--progress", percentage.toFixed(3)); + seekTrack.setAttribute("aria-valuenow", String(Math.round(percentage))); + $("#elapsed").textContent = formatTime(state.position); + $("#duration").textContent = formatTime(state.duration); + } + + function seekFromPointer(event) { + if (state.fixture !== "local" || !currentTrack()) return; + const bounds = seekTrack.getBoundingClientRect(); + const fraction = clamp((event.clientX - bounds.left) / bounds.width, 0, 1); + state.position = state.duration * fraction; + updateSeekVisual(); + updateInspection(`Seek ${formatTime(state.position)} / ${formatTime(state.duration)}`); + } + + function selectTrack(index, play = false) { + if (!state.tracks.length) return; + state.selected = clamp(index, 0, state.tracks.length - 1); + state.position = 0; + if (play) state.playing = true; + renderInfo(); + renderRows(); + renderControls(); + ensureSelectedVisible(); + } + + function ensureSelectedVisible() { + const row = rows.children[state.selected]; + if (row) row.scrollIntoView({block: "nearest"}); + } + + function moveTrack(delta) { + if (!state.tracks.length) return; + const next = (state.selected + delta + state.tracks.length) % state.tracks.length; + selectTrack(next, true); + announce(`${delta < 0 ? "Previous" : "Next"}: ${currentTrack().title}`); + } + + function togglePlay() { + if (!currentTrack()) return; + state.playing = !state.playing; + renderRows(); + renderControls(); + announce(state.playing ? "Playback resumed" : "Playback paused"); + } + + function setVolume(value, message = true) { + state.volume = Math.round(clamp(value, 0, 100)); + renderControls(); + if (message) updateInspection(`Player volume ${state.volume}`); + } + + function volumeFromPointer(event) { + const bounds = volumeHit.getBoundingClientRect(); + const x = event.clientX - (bounds.left + bounds.width / 2); + const y = event.clientY - (bounds.top + bounds.height / 2); + let angle = Math.atan2(y, x) * 180 / Math.PI; + if (angle < 150) angle += 360; + setVolume(((clamp(angle, 150, 390) - 150) / 240) * 100, false); + } + + function updateVolumeSegments() { + const points = [ + [885, 630], [871, 608], [861, 583], [857, 557], [857, 531], [864, 507], + [875, 484], [890, 464], [906, 448], [924, 440], [947, 432], [972, 431], + [996, 433], [1018, 440], [1038, 452], [1055, 467], [1068, 486], [1079, 507], + [1086, 532], [1087, 557], [1085, 582], [1078, 608], [1059, 628] + ]; + const count = points.length; + const lit = Math.round((state.volume / 100) * (count - 1)); + $("#volumeMask").innerHTML = points.map(([x, y]) => + `<circle cx="${x}" cy="${y}" r="5.3" fill="#0d0e0c" stroke="#655039" stroke-width=".8"/>` + ).join(""); + const circles = points.map(([x, y], index) => { + const active = index <= lit; + if (active) { + return `<g><circle cx="${x}" cy="${y}" r="10" fill="#ff9d24" fill-opacity=".28"/><circle cx="${x}" cy="${y}" r="7" fill="#ffad32" fill-opacity=".22"/><circle cx="${x}" cy="${y}" r="4.8" fill="url(#volumeLamp)" stroke="#fff1c2" stroke-width=".85"/></g>`; + } + return ""; + }).join(""); + $("#volumeSegments").innerHTML = circles; + } + + function meterMarkup(x, id) { + const labels = ["-40", "-20", "-10", "-6", "-3", "0", "+3"]; + const ticks = Array.from({length: 19}, (_, index) => { + const angle = -56 + index * (112 / 18); + const major = index % 3 === 0; + const radians = angle * Math.PI / 180; + const x1 = 115 + Math.sin(radians) * (major ? 91 : 95); + const y1 = 131 - Math.cos(radians) * (major ? 91 : 95); + const x2 = 115 + Math.sin(radians) * 102; + const y2 = 131 - Math.cos(radians) * 102; + return `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${index > 14 ? "#d43b25" : "#d7c48d"}" stroke-width="${major ? 1.7 : 1}"/>`; + }).join(""); + const labelText = labels.map((label, index) => { + const angle = -52 + index * (104 / (labels.length - 1)); + const radians = angle * Math.PI / 180; + const lx = 115 + Math.sin(radians) * 72; + const ly = 131 - Math.cos(radians) * 72; + return `<text x="${lx}" y="${ly}" text-anchor="middle" fill="${index > 4 ? "#de442f" : "#ddd0aa"}" font-family="Berkeley Mono,monospace" font-size="10">${label}</text>`; + }).join(""); + return `<g transform="translate(${x} 432)"> + <rect x="0" y="0" width="230" height="113" rx="7" fill="url(#meterFrame)"/> + <rect x="4" y="4" width="222" height="105" rx="5" fill="#050605" stroke="#1b1812" stroke-width="2"/> + <rect x="5" y="5" width="220" height="103" rx="5" fill="url(#meterGlow)"/> + ${ticks}${labelText} + <text x="115" y="91" text-anchor="middle" fill="#e7d7ae" font-family="Berkeley Mono,monospace" font-size="18">VU</text> + <circle cx="115" cy="111" r="17" fill="url(#meterGlow)" opacity=".9"/> + <line id="needle-${id}" x1="115" y1="105" x2="115" y2="27" stroke="#f3d59c" stroke-width="2.2" transform="rotate(0 115 105)"/> + <circle cx="115" cy="105" r="3.2" fill="#c7a56f"/> + </g>`; + } + + function initMeters() { + $("#meters").innerHTML = meterMarkup(112, "left") + meterMarkup(365, "right"); + } + + function vuAngle(db) { + const normalized = clamp((db + 40) / 43, 0, 1); + return -54 + normalized * 108; + } + + function updateMeters(timestamp) { + const started = performance.now(); + const elapsed = Math.min(.05, (timestamp - lastMeterTime) / 1000); + lastMeterTime = timestamp; + const signal = state.playing && currentTrack(); + const base = timestamp / 350; + const targets = signal ? { + left: -13 + Math.sin(base * 1.13) * 5 + Math.sin(base * 2.31) * 2, + right: -14 + Math.sin(base * .97 + 1.1) * 5.5 + Math.sin(base * 2.08) * 2 + } : {left: -40, right: -40}; + for (const side of ["left", "right"]) { + const target = targets[side]; + const speed = target > state.vu[side] ? 7.2 : 2.4; + state.vu[side] += (target - state.vu[side]) * Math.min(1, elapsed * speed); + $(`#needle-${side}`).setAttribute("transform", `rotate(${vuAngle(state.vu[side]).toFixed(2)} 115 105)`); + } + recordRender(performance.now() - started); + requestAnimationFrame(updateMeters); + } + + function updateScrollbar() { + const overflow = scroll.scrollHeight - scroll.clientHeight; + rail.hidden = overflow <= 1; + if (overflow <= 1) return; + const railHeight = rail.clientHeight; + const ratio = scroll.clientHeight / scroll.scrollHeight; + const thumbHeight = Math.max(38, railHeight * ratio); + const travel = railHeight - thumbHeight; + thumb.style.height = `${thumbHeight}px`; + thumb.style.top = `${travel * (scroll.scrollTop / overflow)}px`; + } + + function updateInspection(message = null) { + const label = state.fixture === "radio" ? "Radio fixture" : "Local fixture"; + const play = state.playing ? "playing" : "paused"; + $("#interactionStatus").textContent = message || `${label} · ${play} · volume ${state.volume}`; + } + + function handlePlaylistAction(action) { + if (action === "add") { + const number = state.tracks.length + 1; + state.tracks.push({id: `added-${Date.now()}`, title: "Newly Added Track With a Deliberately Long Display Name", artist: "Prototype Library", meta: "04:12"}); + state.selected = number - 1; + renderAll(); + ensureSelectedVisible(); + announce(`Added track ${number}`); + } else if (action === "new") { + state.playlistName = "UNTITLED"; + state.tracks = []; + state.selected = 0; + state.playing = false; + renderAll(); + announce("New empty playlist"); + } else if (action === "load") { + switchFixture(state.fixture, `Loaded ${fixtures[state.fixture].name}`); + } else if (action === "save") { + announce(`Saved ${state.playlistName || "UNTITLED"} · ${state.tracks.length} tracks`); + } else if (action === "delete") { + $("#deletePrompt").textContent = `Delete “${state.playlistName || "UNTITLED"}”? This removes the saved playlist and leaves an untitled empty queue.`; + deleteDialog.showModal(); + } + } + + function handleTransport(action) { + if (action === "prev") moveTrack(-1); + if (action === "next") moveTrack(1); + if (action === "play") togglePlay(); + if (action === "stop") { + state.playing = false; + state.position = 0; + renderRows(); + renderControls(); + if (state.fixture === "local") updateSeekVisual(); + announce("Playback stopped"); + } + } + + function radioSearch(kind) { + const label = kind === "name" ? "Name search · Groove" : "Tag search · Ambient"; + if (state.fixture !== "radio") switchFixture("radio", label); + state.search = kind; + state.playlistName = label; + renderHeader(); + announce(label); + } + + function recordRender(value) { + if (!state.benchmark) return; + state.benchmark.samples.push(value); + } + + function percentile(sorted, fraction) { + return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * fraction))] || 0; + } + + function updateBenchmarkReadout() { + if (!state.benchmark) return; + const elapsed = performance.now() - state.benchmark.started; + const remaining = Math.max(0, state.benchmark.duration - elapsed); + const sorted = [...state.benchmark.samples].sort((a, b) => a - b); + const median = percentile(sorted, .5); + const p95 = percentile(sorted, .95); + $("#benchmarkReadout").textContent = remaining > 0 + ? `Benchmark ${Math.ceil(remaining / 1000)}s · median ${median.toFixed(2)} ms · p95 ${p95.toFixed(2)} ms` + : `Complete · ${sorted.length} updates · median ${median.toFixed(2)} ms · p95 ${p95.toFixed(2)} ms`; + if (remaining > 0) setTimeout(updateBenchmarkReadout, 250); + else { + state.benchmark.complete = true; + $("#benchmarkButton").disabled = false; + document.documentElement.dataset.benchmark = JSON.stringify({ + samples: sorted.length, + medianMs: Number(median.toFixed(3)), + p95Ms: Number(p95.toFixed(3)), + durationMs: state.benchmark.duration, + viewport: `${Math.round(receiver.getBoundingClientRect().width)}x${Math.round(receiver.getBoundingClientRect().height)}` + }); + announce("60-second dynamic-update benchmark complete"); + } + } + + function runBenchmark(duration = 60000) { + if (state.benchmark && !state.benchmark.complete) return; + state.benchmark = {started: performance.now(), duration, samples: [], complete: false}; + $("#benchmarkButton").disabled = true; + updateBenchmarkReadout(); + } + + function bindEvents() { + $("#localFixture").addEventListener("click", () => switchFixture("local")); + $("#radioFixture").addEventListener("click", () => switchFixture("radio")); + $("#restoreFixture").addEventListener("click", () => switchFixture(state.fixture)); + $("#benchmarkButton").addEventListener("click", () => runBenchmark()); + + $$("[data-transport]").forEach(button => button.addEventListener("click", () => handleTransport(button.dataset.transport))); + $$("[data-mode]").forEach(button => button.addEventListener("click", () => { + const mode = button.dataset.mode; + state.modes[mode] = !state.modes[mode]; + renderControls(); + announce(`${mode.toUpperCase()} ${state.modes[mode] ? "enabled" : "disabled"}`); + })); + $$("[data-radio]").forEach(button => button.addEventListener("click", () => radioSearch(button.dataset.radio))); + $$("[data-action]").forEach(button => button.addEventListener("click", () => handlePlaylistAction(button.dataset.action))); + + seekTrack.addEventListener("pointerdown", event => { + seekDragging = true; + seekTrack.setPointerCapture(event.pointerId); + seekFromPointer(event); + }); + seekTrack.addEventListener("pointermove", event => { if (seekDragging) seekFromPointer(event); }); + seekTrack.addEventListener("pointerup", event => { + seekDragging = false; + seekTrack.releasePointerCapture(event.pointerId); + announce(`Seeked to ${formatTime(state.position)}`); + }); + seekTrack.addEventListener("keydown", event => { + if (!["ArrowLeft", "ArrowRight"].includes(event.key)) return; + event.preventDefault(); + state.position = clamp(state.position + (event.key === "ArrowRight" ? 5 : -5), 0, state.duration); + updateSeekVisual(); + }); + + volumeHit.addEventListener("pointerdown", event => { + volumeDragging = true; + volumeHit.setPointerCapture(event.pointerId); + volumeFromPointer(event); + }); + volumeHit.addEventListener("pointermove", event => { if (volumeDragging) volumeFromPointer(event); }); + volumeHit.addEventListener("pointerup", event => { + volumeDragging = false; + volumeHit.releasePointerCapture(event.pointerId); + announce(`Player volume ${state.volume}`); + }); + volumeHit.addEventListener("wheel", event => { + event.preventDefault(); + setVolume(state.volume + (event.deltaY < 0 ? 5 : -5)); + }, {passive: false}); + volumeHit.addEventListener("keydown", event => { + if (!["ArrowLeft", "ArrowDown", "ArrowRight", "ArrowUp"].includes(event.key)) return; + event.preventDefault(); + setVolume(state.volume + (["ArrowRight", "ArrowUp"].includes(event.key) ? 5 : -5)); + }); + + scroll.addEventListener("scroll", updateScrollbar); + thumb.addEventListener("pointerdown", event => { + thumbDragging = true; + thumbGrabOffset = event.clientY - thumb.getBoundingClientRect().top; + thumb.setPointerCapture(event.pointerId); + }); + thumb.addEventListener("pointermove", event => { + if (!thumbDragging) return; + const railBox = rail.getBoundingClientRect(); + const thumbHeight = thumb.getBoundingClientRect().height; + const travel = railBox.height - thumbHeight; + const top = clamp(event.clientY - railBox.top - thumbGrabOffset, 0, travel); + scroll.scrollTop = (top / travel) * (scroll.scrollHeight - scroll.clientHeight); + }); + thumb.addEventListener("pointerup", event => { + thumbDragging = false; + thumb.releasePointerCapture(event.pointerId); + }); + + deleteDialog.addEventListener("close", () => { + if (deleteDialog.returnValue !== "confirm") return; + const deleted = state.playlistName || "UNTITLED"; + state.playlistName = "UNTITLED"; + state.tracks = []; + state.selected = 0; + state.playing = false; + renderAll(); + announce(`Deleted ${deleted}`); + }); + + document.addEventListener("keydown", event => { + if (deleteDialog.open || event.target.matches("button, input, [role=slider], .playlist-scroll")) return; + if (event.code === "Space") { event.preventDefault(); togglePlay(); } + else if (event.key === "ArrowDown") { event.preventDefault(); selectTrack(state.selected + 1); ensureSelectedVisible(); } + else if (event.key === "ArrowUp") { event.preventDefault(); selectTrack(state.selected - 1); ensureSelectedVisible(); } + else if (event.key === "PageDown") { event.preventDefault(); selectTrack(state.selected + VISIBLE_ROWS); ensureSelectedVisible(); } + else if (event.key === "PageUp") { event.preventDefault(); selectTrack(state.selected - VISIBLE_ROWS); ensureSelectedVisible(); } + else if (event.key === "Home") { event.preventDefault(); selectTrack(0); ensureSelectedVisible(); } + else if (event.key === "End") { event.preventDefault(); selectTrack(state.tracks.length - 1); ensureSelectedVisible(); } + else if (["r", "s", "x", "c"].includes(event.key.toLowerCase())) { + const mode = ({r: "repeat", s: "single", x: "random", c: "consume"})[event.key.toLowerCase()]; + state.modes[mode] = !state.modes[mode]; + renderControls(); + } + }); + + window.addEventListener("resize", () => { + renderRows(); + updateScrollbar(); + }); + } + + function runSelfTest() { + const checks = []; + const check = (name, condition) => { + checks.push({name, passed: Boolean(condition)}); + if (!condition) throw new Error(`Self-test failed: ${name}`); + }; + try { + switchFixture("radio", "Self-test radio fixture"); + check("radio hides seek", $("#seekLine").hidden); + check("radio has two ON AIR indications", !$("#onAirUnderArt").hidden && $(".playlist-row.selected .row-meta")?.textContent === "ON AIR"); + $('[data-mode="repeat"]').click(); + check("mode latches", state.modes.repeat); + $('[data-transport="next"]').click(); + check("next selects a station", state.selected === 1); + $('[data-action="add"]').click(); + check("add changes queue", state.tracks.length === 19); + $('[data-action="new"]').click(); + check("new clears queue", state.tracks.length === 0 && state.playlistName === "UNTITLED"); + $('[data-action="load"]').click(); + check("load restores queue", state.tracks.length === 18); + switchFixture("local", "Self-test local fixture"); + check("local shows seek", !$("#seekLine").hidden); + setVolume(35, false); + check("volume state changes", state.volume === 35); + state.position = state.duration * .72; + updateSeekVisual(); + check("seek state changes", Math.round(Number(seekTrack.getAttribute("aria-valuenow"))) === 72); + scroll.scrollTop = scroll.scrollHeight; + updateScrollbar(); + check("long playlist has scrollbar", !rail.hidden && scroll.scrollTop > 0); + check("fourteen rows fit viewport", Math.abs(rows.children[0].getBoundingClientRect().height * VISIBLE_ROWS - scroll.clientHeight) < 2); + switchFixture("local", "Self-test restored"); + document.documentElement.dataset.selftest = JSON.stringify({passed: true, checks}); + } catch (error) { + document.documentElement.dataset.selftest = JSON.stringify({passed: false, error: error.message, checks}); + console.error(error); + } + } + + initMeters(); + bindEvents(); + renderAll(); + requestAnimationFrame(updateMeters); + + const params = new URLSearchParams(location.search); + if (params.get("state") === "radio") switchFixture("radio"); + if (params.has("benchmark")) runBenchmark(Number(params.get("benchmark")) || 60000); + if (params.has("selftest")) setTimeout(runSelfTest, 50); +})(); +</script> +</body> +</html> diff --git a/working/music-svg-app-design/references/29-alignment-guide.png b/working/music-svg-app-design/references/29-alignment-guide.png Binary files differnew file mode 100644 index 00000000..d8da4ebf --- /dev/null +++ b/working/music-svg-app-design/references/29-alignment-guide.png diff --git a/working/music-svg-app-design/references/dupre-gallery-tall.png b/working/music-svg-app-design/references/dupre-gallery-tall.png Binary files differnew file mode 100644 index 00000000..0f42c2ae --- /dev/null +++ b/working/music-svg-app-design/references/dupre-gallery-tall.png diff --git a/working/music-svg-app-design/references/dupre-gallery.png b/working/music-svg-app-design/references/dupre-gallery.png Binary files differnew file mode 100644 index 00000000..1842fb5a --- /dev/null +++ b/working/music-svg-app-design/references/dupre-gallery.png diff --git a/working/music-svg-app-design/references/waybar-redesign-tall.png b/working/music-svg-app-design/references/waybar-redesign-tall.png Binary files differnew file mode 100644 index 00000000..554dc041 --- /dev/null +++ b/working/music-svg-app-design/references/waybar-redesign-tall.png diff --git a/working/music-svg-app-design/references/waybar-redesign.png b/working/music-svg-app-design/references/waybar-redesign.png Binary files differnew file mode 100644 index 00000000..17e4c307 --- /dev/null +++ b/working/music-svg-app-design/references/waybar-redesign.png |
