As-built: arcade buttons, use-driven control layout, rear I/O, compact case

Reworked the as-built panel around how it's actually used:

- Buttons: replaced the tactile/keycap ("rubber key") buttons with glossy
  arcade pushbuttons, colour-keyed — cyan PREV/NEXT, a big green/red PLAY,
  amber TAP.
- Layout for use: the rotary encoder sits BELOW the screen (so turning it
  never hides the readout) with the buttons spread edge-to-edge underneath —
  PREV far left, NEXT far right, a bigger central PLAY (+ TAP) — so you're
  far less likely to hit the wrong one mid-performance.
- Rear I/O: external trigger in (footswitch), a 1/4" instrument pass-through
  with the click injected, and a shared 1/4" balanced-TRS main out; plus the
  monitor speaker and USB-C, each a labelled jack with a tooltip.
- Size: shrank the case to hug its content (max-width 560→380, trimmed
  padding and inter-section margins) — the dead margin is gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-05-26 07:34:40 -05:00
parent 4340f1838e
commit 4f76783af4

View file

@ -14,8 +14,12 @@
• a 4×16 WS2812 ("NeoPixel") RGB matrix (PIO-driven): the bottom row is the • a 4×16 WS2812 ("NeoPixel") RGB matrix (PIO-driven): the bottom row is the
beat (cyan downbeats / amber group-starts), and the three rows above stack beat (cyan downbeats / amber group-starts), and the three rows above stack
the current beat's subdivisions as they pass (driven by the finest lane); the current beat's subdivisions as they pass (driven by the finest lane);
• an EC11 rotary encoder (turn it: wheel or drag) for tempo, tactile buttons, • controls arranged for use: an EC11 encoder below the screen (turning it
a MAX98357A-style speaker, USB-C and a PWR LED in a matte 3D-printed case. never hides the readout) with arcade pushbuttons spread below — PREV far
left, NEXT far right, a big central PLAY — so you don't hit the wrong one;
• rear I/O: external trigger in (footswitch), a 1/4" instrument pass-through
with the click injected, and a shared 1/4" balanced-TRS main out; plus a
MAX98357A-style monitor speaker, USB-C and a PWR LED, in a compact matte case.
Compare with the idealized /player.html. One file, no deps; shares src/engine.js. Compare with the idealized /player.html. One file, no deps; shares src/engine.js.
--> -->
<script> <script>
@ -43,13 +47,13 @@
--panel-bg:#ffffff; --panel-bd:#d2dae4; --field-bg:#f1f4f8; --field-bd:#cdd6e0; --panel-bg:#ffffff; --panel-bd:#d2dae4; --field-bg:#f1f4f8; --field-bd:#cdd6e0;
} }
body{ body{
margin:0; min-height:100vh; padding:28px 16px 48px; margin:0; min-height:100vh; padding:22px 12px 40px;
background:radial-gradient(circle at 50% -8%, var(--bg1), var(--bg2)); background:radial-gradient(circle at 50% -8%, var(--bg1), var(--bg2));
color:var(--txt); color:var(--txt);
display:flex; flex-direction:column; align-items:center; gap:20px; display:flex; flex-direction:column; align-items:center; gap:14px;
} }
a{color:var(--link)} a{color:var(--link)}
.topbar{width:100%; max-width:560px; display:flex; align-items:center; justify-content:space-between; gap:10px; font-size:13px; color:var(--muted); flex-wrap:wrap} .topbar{width:100%; max-width:380px; display:flex; align-items:center; justify-content:space-between; gap:10px; font-size:13px; color:var(--muted); flex-wrap:wrap}
.topbar b{color:var(--txt)} .topbar b{color:var(--txt)}
.topbar-right{ display:flex; align-items:center; gap:12px } .topbar-right{ display:flex; align-items:center; gap:12px }
.tbtn{ background:transparent; color:var(--muted); border:1px solid var(--panel-bd); border-radius:8px; .tbtn{ background:transparent; color:var(--muted); border:1px solid var(--panel-bd); border-radius:8px;
@ -58,11 +62,11 @@
/* ---- the device: matte 3D-printed case ---- */ /* ---- the device: matte 3D-printed case ---- */
.device{ .device{
width:100%; max-width:560px; position:relative; width:100%; max-width:380px; position:relative;
background: background:
repeating-linear-gradient(115deg, rgba(255,255,255,.012) 0 2px, transparent 2px 4px), repeating-linear-gradient(115deg, rgba(255,255,255,.012) 0 2px, transparent 2px 4px),
linear-gradient(180deg, var(--case), var(--case2)); linear-gradient(180deg, var(--case), var(--case2));
border:1px solid var(--device-bd); border-radius:18px; padding:26px 24px 22px; border:1px solid var(--device-bd); border-radius:18px; padding:16px 14px 14px;
box-shadow:0 24px 55px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,255,255,.05), inset 0 -2px 10px rgba(0,0,0,.6); box-shadow:0 24px 55px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,255,255,.05), inset 0 -2px 10px rgba(0,0,0,.6);
} }
.device::before, .device::after, .device .screw{ content:""; position:absolute; width:10px; height:10px; border-radius:50%; .device::before, .device::after, .device .screw{ content:""; position:absolute; width:10px; height:10px; border-radius:50%;
@ -70,7 +74,7 @@
.device::before{ top:12px; left:12px } .device::after{ top:12px; right:12px } .device::before{ top:12px; left:12px } .device::after{ top:12px; right:12px }
.screw.bl{ bottom:12px; left:12px } .screw.br{ bottom:12px; right:12px } .screw.bl{ bottom:12px; left:12px } .screw.br{ bottom:12px; right:12px }
.brandrow{ display:flex; align-items:flex-end; justify-content:space-between; margin:0 4px 16px; } .brandrow{ display:flex; align-items:flex-end; justify-content:space-between; margin:0 2px 12px; }
.silk{ color:var(--silk); letter-spacing:.04em } .silk{ color:var(--silk); letter-spacing:.04em }
.silk .vk{ font-weight:800; letter-spacing:.16em; font-size:15px; color:var(--silk) } .silk .vk{ font-weight:800; letter-spacing:.16em; font-size:15px; color:var(--silk) }
.silk .model{ font-size:10px; text-transform:uppercase; letter-spacing:.18em; opacity:.8 } .silk .model{ font-size:10px; text-transform:uppercase; letter-spacing:.18em; opacity:.8 }
@ -86,45 +90,55 @@
.tft-cap{ text-align:center; font-size:10px; color:var(--muted); margin-top:7px; letter-spacing:.02em } .tft-cap{ text-align:center; font-size:10px; color:var(--muted); margin-top:7px; letter-spacing:.02em }
/* ---- 4×16 WS2812 RGB matrix: bottom row = beat, 3 rows above = subdivisions ---- */ /* ---- 4×16 WS2812 RGB matrix: bottom row = beat, 3 rows above = subdivisions ---- */
.ledgrid{ display:flex; flex-direction:column; gap:5px; width:max-content; margin:16px auto 4px; .ledgrid{ display:flex; flex-direction:column; gap:4px; width:max-content; margin:12px auto 3px;
background:linear-gradient(180deg,#10221c,var(--pcb)); border:1px solid #07140f; border-radius:5px; padding:8px 9px; background:linear-gradient(180deg,#10221c,var(--pcb)); border:1px solid #07140f; border-radius:5px; padding:8px 9px;
box-shadow:inset 0 1px 2px rgba(0,0,0,.6) } box-shadow:inset 0 1px 2px rgba(0,0,0,.6) }
.ledrow{ display:flex; gap:5px } .ledrow{ display:flex; gap:4px }
.ledrow.beatrow{ margin-top:4px; padding-top:6px; border-top:1px solid rgba(255,255,255,.07) } .ledrow.beatrow{ margin-top:4px; padding-top:6px; border-top:1px solid rgba(255,255,255,.07) }
.npx{ width:15px; height:15px; border-radius:3px; background:#0c0e10; border:1px solid #060708; .npx{ width:15px; height:15px; border-radius:3px; background:#0c0e10; border:1px solid #060708;
position:relative; transition:background .05s, box-shadow .05s } position:relative; transition:background .05s, box-shadow .05s }
.npx::after{ content:""; position:absolute; inset:4px; border-radius:1px; background:rgba(255,255,255,.05) } /* the 5050 die */ .npx::after{ content:""; position:absolute; inset:4px; border-radius:1px; background:rgba(255,255,255,.05) } /* the 5050 die */
.ledbar-cap{ text-align:center; font-size:10px; color:var(--muted); margin:2px 0 0; letter-spacing:.02em } .ledbar-cap{ text-align:center; font-size:10px; color:var(--muted); margin:2px 0 0; letter-spacing:.02em }
/* ---- controls: encoder + tactile buttons ---- */ /* ---- controls: encoder above (under the screen), arcade buttons spread below ----
.controls{ display:flex; align-items:center; justify-content:center; gap:14px; margin:16px 4px 4px; flex-wrap:wrap } wheel never hides the readout · PREV far-left / NEXT far-right · big central PLAY */
.keys{ display:flex; gap:9px; flex-wrap:wrap; justify-content:center } .controls{ display:flex; flex-direction:column; align-items:center; gap:13px; margin:14px 0 2px }
.key{ display:flex; flex-direction:column; align-items:center; gap:4px } .enc-wrap{ display:flex; flex-direction:column; align-items:center; gap:5px }
.cap{ width:46px; height:40px; border-radius:7px; background:linear-gradient(180deg,#2c333d,#1a1f27); .enc{ width:52px; height:52px; border-radius:50%; cursor:ns-resize; position:relative; touch-action:none;
border:1px solid #3a424e; color:#d4dbe4; font-size:15px; cursor:pointer; display:flex; align-items:center; justify-content:center;
box-shadow:0 3px 0 #0b0e12, inset 0 1px 0 rgba(255,255,255,.07); user-select:none; transition:transform .04s, box-shadow .04s }
.cap:active{ transform:translateY(2px); box-shadow:0 1px 0 #0b0e12, inset 0 1px 0 rgba(255,255,255,.07) }
.cap.play{ background:linear-gradient(180deg,#1f7a4d,#155f3b); border-color:#2e7d32; color:#eafff3 }
.cap.play.on{ background:linear-gradient(180deg,#b23b3b,#8f2d2d); border-color:#c0392b }
.key small{ font-size:8px; color:var(--silk); letter-spacing:.1em; text-transform:uppercase; opacity:.8 }
.enc-wrap{ display:flex; flex-direction:column; align-items:center; gap:4px }
.enc{ width:54px; height:54px; border-radius:50%; cursor:ns-resize; position:relative; touch-action:none;
background:repeating-conic-gradient(from 0deg, #424b57 0 7deg, #2c333d 7deg 14deg); background:repeating-conic-gradient(from 0deg, #424b57 0 7deg, #2c333d 7deg 14deg);
border:2px solid #565f6c; box-shadow:0 3px 8px rgba(0,0,0,.5), inset 0 1px 1px rgba(255,255,255,.12) } border:2px solid #565f6c; box-shadow:0 3px 8px rgba(0,0,0,.5), inset 0 1px 1px rgba(255,255,255,.12) }
.enc::before{ content:""; position:absolute; inset:9px; border-radius:50%; background:radial-gradient(circle at 38% 32%,#3b434f,#181c22 75%) } .enc::before{ content:""; position:absolute; inset:9px; border-radius:50%; background:radial-gradient(circle at 38% 32%,#3b434f,#181c22 75%) }
.enc::after{ content:""; position:absolute; left:50%; top:7px; width:3px; height:13px; background:var(--cyan); border-radius:2px; .enc::after{ content:""; position:absolute; left:50%; top:7px; width:3px; height:12px; background:var(--cyan); border-radius:2px;
transform-origin:50% 20px; transform:translateX(-50%) rotate(var(--a,0deg)); box-shadow:0 0 5px var(--cyan) } transform-origin:50% 19px; transform:translateX(-50%) rotate(var(--a,0deg)); box-shadow:0 0 5px var(--cyan) }
.enc-wrap small{ font-size:8px; color:var(--silk); letter-spacing:.12em; opacity:.8 } .enc-wrap small{ font-size:8px; color:var(--silk); letter-spacing:.12em; opacity:.85 }
.keys{ display:flex; align-items:flex-end; justify-content:space-between; width:100%; padding:0 2px }
.key-mid{ display:flex; align-items:flex-end; gap:20px }
.key{ display:flex; flex-direction:column; align-items:center; gap:6px }
.abtn{ width:50px; height:50px; border-radius:50%; border:0; padding:0; cursor:pointer; position:relative; color:#fff; font-size:18px; line-height:1;
background:radial-gradient(circle at 36% 30%, rgba(255,255,255,.6), rgba(255,255,255,0) 42%), radial-gradient(circle at 50% 64%, var(--c1,#33d0ff), var(--c2,#0a7fb0));
box-shadow:0 0 0 3px #0b0d11, 0 0 0 4px #363c46, 0 5px 7px rgba(0,0,0,.5), inset 0 -3px 6px rgba(0,0,0,.35), inset 0 2px 4px rgba(255,255,255,.28);
text-shadow:0 1px 2px rgba(0,0,0,.45); user-select:none; transition:transform .05s, box-shadow .05s, filter .05s }
.abtn:active{ transform:translateY(2px); filter:brightness(.92);
box-shadow:0 0 0 3px #0b0d11, 0 0 0 4px #363c46, 0 2px 3px rgba(0,0,0,.5), inset 0 -2px 4px rgba(0,0,0,.4), inset 0 2px 4px rgba(255,255,255,.2) }
.abtn.nav{ --c1:#33d0ff; --c2:#0a7fb0 }
.abtn.tap{ --c1:#ffd56a; --c2:#c98a1f; color:#3a2a00; text-shadow:0 1px 1px rgba(255,255,255,.35); font-size:12px; font-weight:800; letter-spacing:.04em }
.abtn.play{ --c1:#4ce08e; --c2:#178f49; width:66px; height:66px; font-size:26px }
.abtn.play.on{ --c1:#ff6a6a; --c2:#a82828 }
.key small{ font-size:8px; color:var(--silk); letter-spacing:.1em; text-transform:uppercase; opacity:.85 }
/* ---- speaker grille + ports ---- */ /* ---- monitor speaker + rear I/O (1/4" jacks + USB-C) ---- */
.footrow{ display:flex; align-items:center; justify-content:space-between; margin:18px 6px 2px } .grille{ height:11px; margin:13px 8px 9px; border-radius:5px;
.grille{ flex:1; height:12px; margin-right:12px; border-radius:5px;
background:radial-gradient(circle, #000 1.1px, transparent 1.4px) 0 0/8px 8px; opacity:.5 } background:radial-gradient(circle, #000 1.1px, transparent 1.4px) 0 0/8px 8px; opacity:.5 }
.usbc{ font-size:8px; color:var(--silk); letter-spacing:.12em; opacity:.7; display:flex; align-items:center; gap:5px } .io{ display:flex; align-items:flex-start; justify-content:space-between; gap:6px; margin:0 2px;
.usbc .port{ width:22px; height:8px; border-radius:4px; background:#0a0c0f; border:1px solid #000; box-shadow:inset 0 0 2px #000 } padding:9px 8px 8px; border-radius:9px; background:#0c0f14; border:1px solid #05070a; box-shadow:inset 0 1px 3px rgba(0,0,0,.6) }
.jack{ flex:1; display:flex; flex-direction:column; align-items:center; gap:5px }
.jack i{ width:20px; height:20px; border-radius:50%; background:radial-gradient(circle at 40% 34%, #333a44, #07090c 72%);
border:2px solid #5b6470; box-shadow:inset 0 0 4px #000 }
.jack b{ font-size:7.5px; font-weight:700; color:var(--silk); letter-spacing:.05em; text-transform:uppercase; opacity:.85; text-align:center; line-height:1.3 }
.jack.usb i{ width:24px; height:10px; border-radius:4px; border:2px solid #5b6470; background:#07090c; margin-top:5px }
/* ---- load panel (same as the other pages) ---- */ /* ---- load panel (same as the other pages) ---- */
.panel{ width:100%; max-width:560px; background:var(--panel-bg); border:1px solid var(--panel-bd); border-radius:14px; padding:16px } .panel{ width:100%; max-width:380px; background:var(--panel-bg); border:1px solid var(--panel-bd); border-radius:14px; padding:16px }
.panel h2{ margin:0 0 4px; font-size:15px } .panel h2{ margin:0 0 4px; font-size:15px }
.panel p.sub{ margin:0 0 12px; font-size:12px; color:var(--muted); line-height:1.45 } .panel p.sub{ margin:0 0 12px; font-size:12px; color:var(--muted); line-height:1.45 }
textarea{ width:100%; background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd); border-radius:9px; textarea{ width:100%; background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd); border-radius:9px;
@ -170,19 +184,25 @@
<div class="ledbar-cap">4×16 WS2812 — beat (bottom) + 3 subdivision rows</div> <div class="ledbar-cap">4×16 WS2812 — beat (bottom) + 3 subdivision rows</div>
<div class="controls"> <div class="controls">
<div class="keys">
<div class="key"><button class="cap" id="bPrev" title="previous item"></button><small>Prev</small></div>
<div class="key"><button class="cap play" id="bPlay" title="play / stop (Space)"></button><small>Play</small></div>
<div class="key"><button class="cap" id="bNext" title="next item"></button><small>Next</small></div>
<div class="key"><button class="cap" id="bTap" title="tap tempo (T)">TAP</button><small>Tap</small></div>
</div>
<div class="enc-wrap"><div class="enc" id="enc" title="Tempo — scroll or drag to turn"></div><small>TEMPO</small></div> <div class="enc-wrap"><div class="enc" id="enc" title="Tempo — scroll or drag to turn"></div><small>TEMPO</small></div>
<div class="keys">
<div class="key"><button class="abtn nav" id="bPrev" title="previous item"></button><small>Prev</small></div>
<div class="key-mid">
<div class="key"><button class="abtn play" id="bPlay" title="play / stop (Space)"></button><small>Play</small></div>
<div class="key"><button class="abtn tap" id="bTap" title="tap tempo (T)">TAP</button><small>Tap</small></div>
</div>
<div class="key"><button class="abtn nav" id="bNext" title="next item"></button><small>Next</small></div>
</div>
</div> </div>
<div class="footrow">
<div class="grille"></div> <div class="grille"></div>
<div class="usbc"><span class="port"></span>USBC</div> <div class="io">
<div class="jack" title="External trigger in — footswitch to start/stop or tap tempo"><i></i><b>Trig In</b></div>
<div class="jack" title="Instrument in — 1/4&quot; pass-through; the click is mixed into your signal"><i></i><b>Inst In</b></div>
<div class="jack" title="Main out — 1/4&quot; balanced TRS (instrument + click); the shared output plug"><i></i><b>Out TRS</b></div>
<div class="jack usb" title="USB-C — power &amp; set-list transfer"><i></i><b>USB-C</b></div>
</div> </div>
<div class="ledbar-cap">Trig in · 1/4″ inst passthrough (click injected) · shared 1/4″ balancedTRS out</div>
</div> </div>
<!-- ===================== LOAD CONFIG ===================== --> <!-- ===================== LOAD CONFIG ===================== -->