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) } }