Compare commits

..

No commits in common. "00cb245331a1a0dc0f8d5a8bac4366008703c674" and "649501b51cc8f7cd1ab8eeba9e7abee53fcb6074" have entirely different histories.

2 changed files with 23 additions and 43 deletions

View file

@ -1 +1 @@
0.0.16
0.0.15

View file

@ -11,9 +11,8 @@
• 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 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);
• a fixed 16-pixel WS2812 ("NeoPixel") RGB beat bar (PIO-driven) — lights the
first beatsPerBar slots, cyan downbeats / amber group-starts / dim others;
• 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.
@ -87,15 +86,13 @@
background:#000; border-radius:2px }
.oled-cap{ text-align:center; font-size:10px; color:var(--muted); margin-top:6px; letter-spacing:.02em }
/* ---- 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;
/* ---- 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;
box-shadow:inset 0 1px 2px rgba(0,0,0,.6) }
.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 */
.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 */
.ledbar-cap{ text-align:center; font-size:10px; color:var(--muted); margin:2px 0 0; letter-spacing:.02em }
/* ---- controls: encoder + tactile buttons ---- */
@ -168,8 +165,8 @@
</div>
<div class="oled-cap">0.96″ 128×64 mono OLED (SSD1306)</div>
<div class="ledgrid" id="leds"></div>
<div class="ledbar-cap">4×16 WS2812 — beat (bottom) + 3 subdivision rows</div>
<div class="ledbar" id="leds"></div>
<div class="ledbar-cap">16px WS2812 RGB beat bar</div>
<div class="controls">
<div class="keys">
@ -323,36 +320,19 @@ function drawOLED(){
oledThreshold();
}
/* ========== RENDER: 4×16 WS2812 matrix — beat row + 3 subdivision rows ========= */
const LED_COLS=16, LED_ROWS=4, CYAN="#0AB3F7", AMBER="#ffd166", SUB="#2fe0a0";
let gridLeds=[];
function buildBar(){
const box=$("leds"); box.innerHTML=""; gridLeds=[[],[],[],[]];
for(let r=LED_ROWS-1;r>=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<LED_COLS;c++){ const d=document.createElement("div"); d.className="npx"; row.appendChild(d); gridLeds[r][c]=d; }
box.appendChild(row);
}
}
function setPx(led,bg,sh){ led.style.background=bg; led.style.boxShadow=sh||"none"; }
/* ========================= 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<LED_COUNT;i++){ const d=document.createElement("div"); d.className="npx"; box.appendChild(d); } }
function renderBar(){
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<LED_ROWS;r++) for(let c=0;c<LED_COLS;c++){
const led=gridLeds[r][c];
if(c>=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<spb && c===curBeat){ // SUBDIVISION rows climb in the current beat's column
if(curSub>=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)
const m=meters[0], bpb=m?m.beatsPerBar:0;
const cur=(state.running&&m)? Math.floor(m.currentStep/m.stepsPerBeat) : -1;
const els=$("leds").children;
for(let i=0;i<els.length;i++){
const led=els[i];
if(i>=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"; }
}
}