From fcf58d9c1f4e7d71ebe73e9728e632d31975e5fe Mon Sep 17 00:00:00 2001 From: Me Here Date: Tue, 26 May 2026 07:06:28 -0500 Subject: [PATCH] =?UTF-8?q?As-built:=20stack=20the=20beat=20bar=20into=20a?= =?UTF-8?q?=204=C3=9716=20matrix=20(beat=20+=203=20subdivision=20rows)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the single 16-px strip with a 4×16 WS2812 matrix (four 16-px strips, still PIO-driven on the RP2040): - bottom row = the beat (cyan downbeats / amber group-starts, current beat bright, the rest a dim grid) — separated from the rows above by a divider; - the three rows above stack the CURRENT beat's subdivisions as they pass: a column climbs row-by-row with each subdivision and resets on the next beat, with faint "slots" showing the ladder it will climb. Subdivisions are driven by the finest lane that shares lane 1's beat grid (non-poly, same beatsPerBar, max stepsPerBeat) — so an 8th-note hat shows one row, 16ths show three, straight quarters show none. Also fixed a stray "#05measure" typo left in the old .npx border rule. Co-Authored-By: Claude Opus 4.7 (1M context) --- player-asbuilt.html | 64 +++++++++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/player-asbuilt.html b/player-asbuilt.html index d8d20f0..61e560d 100644 --- a/player-asbuilt.html +++ b/player-asbuilt.html @@ -11,8 +11,9 @@ • a 128×64 MONOCHROME OLED (SSD1306/SH1106 class): rendered as a true 1-bit framebuffer (drawn, then thresholded to crisp on/off pixels, scaled with image-rendering:pixelated) so the cramped real layout is honest; - • a fixed 16-pixel WS2812 ("NeoPixel") RGB beat bar (PIO-driven) — lights the - first beatsPerBar slots, cyan downbeats / amber group-starts / dim others; + • 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 + 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, a MAX98357A-style speaker, USB-C and a PWR LED in a matte 3D-printed case. Compare with the idealized /player.html. One file, no deps; shares src/engine.js. @@ -86,13 +87,15 @@ background:#000; border-radius:2px } .oled-cap{ text-align:center; font-size:10px; color:var(--muted); margin-top:6px; letter-spacing:.02em } - /* ---- WS2812 RGB beat bar (fixed 16 px) ---- */ - .ledbar{ display:flex; gap:5px; justify-content:center; align-items:center; margin:16px auto 4px; width:max-content; - background:linear-gradient(180deg,#10221c,var(--pcb)); border:1px solid #07140f; border-radius:5px; padding:7px 9px; + /* ---- 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; + 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) } - .npx{ width:16px; height:16px; border-radius:3px; background:#0c0e10; border:1px solid #05measure; - border:1px solid #060708; position:relative; transition:background .05s, box-shadow .05s } - .npx::after{ content:""; position:absolute; inset:4px; border-radius:1px; background:rgba(255,255,255,.06) } /* the 5050 die */ + .ledrow{ display:flex; gap:5px } + .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; + 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 */ .ledbar-cap{ text-align:center; font-size:10px; color:var(--muted); margin:2px 0 0; letter-spacing:.02em } /* ---- controls: encoder + tactile buttons ---- */ @@ -165,8 +168,8 @@
0.96″ 128×64 mono OLED (SSD1306)
-
-
16‑px WS2812 RGB beat bar
+
+
4×16 WS2812 — beat (bottom) + 3 subdivision rows
@@ -320,19 +323,36 @@ function drawOLED(){ oledThreshold(); } -/* ========================= RENDER: 16-px WS2812 beat bar ===================== */ -const LED_COUNT=16, CYAN="#0AB3F7", AMBER="#ffd166"; -function buildBar(){ const box=$("leds"); box.innerHTML=""; for(let i=0;i=0;r--){ // top row built first → the beat row (r=0) sits at the bottom + const row=document.createElement("div"); row.className="ledrow"+(r===0?" beatrow":""); + for(let c=0;c=bpb){ led.style.background="#0c0e10"; led.style.boxShadow="none"; continue; } // unused pixel on the fixed strip - const grp=m.groupStarts.has(i); - if(i===cur){ const c=grp?AMBER:CYAN; led.style.background=c; led.style.boxShadow="0 0 10px "+c+", inset 0 0 4px #fff"; } - else { led.style.background = grp?"rgba(255,209,102,.30)":"rgba(10,179,247,.20)"; led.style.boxShadow="none"; } + const ref=meters[0], bpb=ref?ref.beatsPerBar:0; + // beats come from lane 1; subdivisions from the finest lane sharing that beat grid + let disp=ref; if(ref){ for(const m of meters){ if(!m.poly && m.beatsPerBar===ref.beatsPerBar && m.stepsPerBeat>disp.stepsPerBeat) disp=m; } } + const spb=disp?disp.stepsPerBeat:1; + let curBeat=-1, curSub=-1; + if(state.running&&disp&&disp.currentStep>=0){ curBeat=Math.floor(disp.currentStep/spb); curSub=disp.currentStep%spb; } + for(let r=0;r=bpb){ setPx(led,"#0c0e10"); continue; } // unused column on the fixed matrix + const grp=ref.groupStarts.has(c); + if(r===0){ // BEAT row (bottom) + if(c===curBeat){ const col=grp?AMBER:CYAN; setPx(led,col,"0 0 9px "+col+", inset 0 0 3px #fff"); } + else setPx(led, grp?"rgba(255,209,102,.26)":"rgba(10,179,247,.15)"); + } else if(r=r) setPx(led,SUB,"0 0 7px "+SUB); // reached this subdivision + else setPx(led,"rgba(47,224,160,.10)"); // ahead: faint slot showing the ladder this beat will climb + } else setPx(led,"#0c0e10"); // off (no such subdivision, or not the current beat) } }