As-built: upgrade the mono OLED to a 320×240 colour IPS TFT (ST7789)
The 128×64 mono OLED was too pixelated, so step up to the realistic next tier: a 2.0″ 320×240 colour IPS TFT (ST7789 — e.g. Pimoroni Pico Display 2.0), ~5× the resolution and full colour. - Drop the 1-bit threshold + image-rendering:pixelated; render on a hi-DPI canvas (backing = 320×240 × devicePixelRatio) with smooth anti-aliased type. - Richer colour layout: dim header (position + green ▶ PLAY / grey ■ STOP), a big cyan tempo with "BPM ♩ <grouping>", the centred item name (ellipsised), and a bottom strip with bar·beat + an amber bars countdown. The screen stays a fixed dark UI (a TFT shows whatever firmware draws); the page chrome still follows light/dark/system. Beat matrix, encoder, buttons unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
00cb245331
commit
b434520505
1 changed files with 50 additions and 53 deletions
|
|
@ -8,9 +8,9 @@
|
|||
<!--
|
||||
"As-built" variant of the PM-1 player: the same firmware/engine, drawn with the
|
||||
parts you'd actually solder for an RP2040 build —
|
||||
• 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 2.0″ 320×240 colour IPS TFT (ST7789, e.g. Pimoroni Pico Display 2.0):
|
||||
the upgrade from the cramped 128×64 mono OLED — full colour, ~5× the
|
||||
resolution, smooth anti-aliased type (rendered hi-DPI on a canvas);
|
||||
• 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);
|
||||
|
|
@ -77,15 +77,13 @@
|
|||
.pwr{ display:flex; align-items:center; gap:6px; font-size:9px; color:var(--silk); text-transform:uppercase; letter-spacing:.14em; opacity:.85 }
|
||||
.pwr .dot{ width:7px; height:7px; border-radius:50%; background:#2fe07a; box-shadow:0 0 7px #2fe07a }
|
||||
|
||||
/* ---- 128×64 monochrome OLED module ---- */
|
||||
.oled-wrap{ display:flex; justify-content:center; margin:0 4px }
|
||||
.oled-mod{ background:#0a0c0f; border:1px solid #000; border-radius:6px; padding:10px 12px;
|
||||
box-shadow:inset 0 0 0 2px #1a1d22, 0 2px 4px rgba(0,0,0,.5); position:relative }
|
||||
.oled-mod::before{ content:""; position:absolute; top:6px; right:6px; width:26px; height:5px; border-radius:2px;
|
||||
background:repeating-linear-gradient(90deg,#2a2f36 0 2px,transparent 2px 4px); opacity:.6 } /* tiny pin header detail */
|
||||
#oled{ display:block; width:336px; height:168px; image-rendering:pixelated; image-rendering:crisp-edges;
|
||||
background:#000; border-radius:2px }
|
||||
.oled-cap{ text-align:center; font-size:10px; color:var(--muted); margin-top:6px; letter-spacing:.02em }
|
||||
/* ---- 2.0″ 320×240 colour IPS TFT (the OLED upgrade) ---- */
|
||||
.tft-wrap{ display:flex; justify-content:center; margin:0 4px }
|
||||
.tft-mod{ background:#000; border-radius:10px; padding:9px;
|
||||
box-shadow:inset 0 0 0 2px #15181d, inset 0 0 0 3px #000, 0 3px 9px rgba(0,0,0,.55) }
|
||||
#tft{ display:block; width:320px; height:240px; max-width:100%; border-radius:4px; background:#06080c;
|
||||
box-shadow:0 0 0 1px #000, inset 0 0 18px rgba(0,0,0,.45) }
|
||||
.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 ---- */
|
||||
.ledgrid{ display:flex; flex-direction:column; gap:5px; width:max-content; margin:16px auto 4px;
|
||||
|
|
@ -161,12 +159,12 @@
|
|||
<div class="pwr"><span class="dot"></span>PWR</div>
|
||||
</div>
|
||||
|
||||
<div class="oled-wrap">
|
||||
<div class="oled-mod">
|
||||
<canvas id="oled" width="128" height="64" aria-label="128 by 64 monochrome OLED"></canvas>
|
||||
<div class="tft-wrap">
|
||||
<div class="tft-mod">
|
||||
<canvas id="tft" width="320" height="240" aria-label="2 inch 320 by 240 colour IPS display"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oled-cap">0.96″ 128×64 mono OLED (SSD1306)</div>
|
||||
<div class="tft-cap">2.0″ 320×240 IPS TFT (ST7789)</div>
|
||||
|
||||
<div class="ledgrid" id="leds"></div>
|
||||
<div class="ledbar-cap">4×16 WS2812 — beat (bottom) + 3 subdivision rows</div>
|
||||
|
|
@ -285,42 +283,41 @@ function tapTempo(){ const now=performance.now(); taps=taps.filter(t=>now-t<2000
|
|||
let knobAngle=0;
|
||||
function nudge(d){ ramp.on=false; setBpm(state.bpm+d); knobAngle+=d*9; $("enc").style.setProperty("--a",knobAngle+"deg"); renderAll(); }
|
||||
|
||||
/* ========================= RENDER: 128×64 mono OLED ========================== */
|
||||
const oled=$("oled"), oc=oled.getContext("2d");
|
||||
const OLED_ON=[150,220,255]; // cool-white/blue monochrome panel
|
||||
let nameScroll=0;
|
||||
function oledThreshold(){ // posterize the framebuffer to 1-bit, recolour to the panel
|
||||
const img=oc.getImageData(0,0,128,64), d=img.data, r=OLED_ON[0], g=OLED_ON[1], b=OLED_ON[2];
|
||||
for(let i=0;i<d.length;i+=4){ const on=d[i]>110; d[i]=on?r:0; d[i+1]=on?g:0; d[i+2]=on?b:0; d[i+3]=255; }
|
||||
oc.putImageData(img,0,0);
|
||||
}
|
||||
function drawOLED(){
|
||||
/* ===================== RENDER: 320×240 colour IPS TFT (ST7789) =============== */
|
||||
const TFT_W=320, TFT_H=240;
|
||||
const tft=$("tft"), tc=tft.getContext("2d");
|
||||
(function(){ const dpr=Math.max(1,Math.min(3,window.devicePixelRatio||1));
|
||||
tft.width=TFT_W*dpr; tft.height=TFT_H*dpr; tc.scale(dpr,dpr); })(); // hi-DPI backing → crisp type
|
||||
function drawTFT(){
|
||||
const m=meters[0];
|
||||
oc.fillStyle="#000"; oc.fillRect(0,0,128,64);
|
||||
oc.fillStyle="#fff"; oc.textBaseline="top";
|
||||
// top status row
|
||||
oc.font='7px "Courier New",monospace'; oc.textAlign="left";
|
||||
oc.fillText(setlist?((idx+1)+"/"+setlist.items.length):"-/-", 0, 0);
|
||||
oc.textAlign="right"; oc.fillText(state.running?"PLAY":"STOP", 128, 0);
|
||||
// big BPM
|
||||
oc.textAlign="left"; oc.font='bold 27px Arial,sans-serif';
|
||||
const bpmStr=String(state.bpm); oc.fillText(bpmStr, 0, 8);
|
||||
const bw=Math.min(oc.measureText(bpmStr).width+5, 96);
|
||||
oc.font='8px "Courier New",monospace'; oc.fillText("BPM", bw, 12);
|
||||
if(m){ oc.fillText((m.groupsStr.replace(/[^0-9+]/g,"")||String(m.beatsPerBar)), bw, 24); } // grouping e.g. 2+2+3
|
||||
oc.fillRect(0,37,128,1); // separator
|
||||
// item name (marquee if too wide)
|
||||
oc.font='8px "Courier New",monospace';
|
||||
const name=setlist?(setlist.items[idx].name||"-"):"-";
|
||||
const nw=oc.measureText(name).width;
|
||||
let nx=0;
|
||||
if(nw>128){ const period=nw+24; nx=128-(nameScroll%period); }
|
||||
oc.fillText(name, nx, 41);
|
||||
// bottom row
|
||||
oc.font='7px "Courier New",monospace'; oc.textAlign="left";
|
||||
oc.fillText(state.running&&m ? ("bar "+(m.currentBar+1)+" beat "+(Math.floor(m.currentStep/m.stepsPerBeat)+1||"-")) : "ready", 0, 53);
|
||||
if(segBars>0){ oc.textAlign="right"; const rem=Math.max(0,segBars-(m?m.currentBar:0)); oc.fillText((state.running?rem:segBars)+"b", 128, 53); }
|
||||
oledThreshold();
|
||||
const g=tc.createLinearGradient(0,0,0,TFT_H); g.addColorStop(0,"#0b0f18"); g.addColorStop(1,"#05070c");
|
||||
tc.fillStyle=g; tc.fillRect(0,0,TFT_W,TFT_H);
|
||||
// header
|
||||
tc.textBaseline="middle"; tc.textAlign="left";
|
||||
tc.font='600 13px "Segoe UI",system-ui,sans-serif'; tc.fillStyle="#5b7a93";
|
||||
tc.fillText("♪ "+(setlist?((idx+1)+"/"+setlist.items.length):"–/–"), 14, 18);
|
||||
tc.textAlign="right"; tc.fillStyle = state.running ? "#2fe07a" : "#6b7787";
|
||||
tc.fillText(state.running?"▶ PLAY":"■ STOP", TFT_W-14, 18);
|
||||
tc.fillStyle="#13283c"; tc.fillRect(14,32,TFT_W-28,1);
|
||||
// tempo — the hero number
|
||||
tc.textBaseline="alphabetic"; tc.textAlign="left";
|
||||
tc.fillStyle="#1fb6f0"; tc.font='800 86px "Segoe UI",system-ui,sans-serif';
|
||||
const bpm=String(state.bpm); tc.fillText(bpm, 18, 134);
|
||||
const bx=18+tc.measureText(bpm).width+9;
|
||||
tc.fillStyle="#5b7a93"; tc.font='700 17px "Segoe UI",system-ui,sans-serif'; tc.fillText("BPM", bx, 96);
|
||||
tc.font='600 18px "Segoe UI",system-ui,sans-serif'; tc.fillText("♩ "+(m?(m.groupsStr.replace(/[^0-9+]/g,"")||m.beatsPerBar):"–"), bx, 122);
|
||||
// item name — centred, ellipsised
|
||||
tc.textAlign="center"; tc.fillStyle="#e9eff7"; tc.font='600 23px "Segoe UI",system-ui,sans-serif';
|
||||
let nm=setlist?(setlist.items[idx].name||"—"):"—";
|
||||
if(tc.measureText(nm).width>TFT_W-28){ while(nm.length>1 && tc.measureText(nm+"…").width>TFT_W-28) nm=nm.slice(0,-1); nm+="…"; }
|
||||
tc.fillText(nm, TFT_W/2, 180);
|
||||
// bottom strip
|
||||
tc.fillStyle="#13283c"; tc.fillRect(14,200,TFT_W-28,1);
|
||||
tc.textAlign="left"; tc.fillStyle="#7f8b9a"; tc.font='600 14px "Segoe UI",system-ui,sans-serif';
|
||||
tc.fillText(state.running&&m ? ("BAR "+(m.currentBar+1)+" BEAT "+(Math.floor(m.currentStep/m.stepsPerBeat)+1||"–")) : "READY", 14, 224);
|
||||
if(segBars>0){ const rem=Math.max(0,segBars-(m?m.currentBar:0));
|
||||
tc.textAlign="right"; tc.fillStyle="#ffd166"; tc.font='700 15px "Segoe UI",system-ui,sans-serif';
|
||||
tc.fillText((state.running?rem:segBars)+" BARS", TFT_W-14, 224); }
|
||||
}
|
||||
|
||||
/* ========== RENDER: 4×16 WS2812 matrix — beat row + 3 subdivision rows ========= */
|
||||
|
|
@ -356,12 +353,12 @@ function renderBar(){
|
|||
}
|
||||
}
|
||||
|
||||
function renderAll(){ drawOLED(); renderBar(); $("bPlay").textContent=state.running?"■":"▶"; $("bPlay").classList.toggle("on",state.running);
|
||||
function renderAll(){ drawTFT(); renderBar(); $("bPlay").textContent=state.running?"■":"▶"; $("bPlay").classList.toggle("on",state.running);
|
||||
$("enc").style.setProperty("--a", knobAngle+"deg"); }
|
||||
function draw(){
|
||||
if(audioCtx&&state.running){ const now=audioCtx.currentTime;
|
||||
for(const m of meters){ while(m.vqPtr<m.vq.length&&m.vq[m.vqPtr].time<=now){ m.currentStep=m.vq[m.vqPtr].step; m.currentBar=m.vq[m.vqPtr].bar; m.vqPtr++; } } }
|
||||
nameScroll+=0.6; drawOLED(); renderBar();
|
||||
drawTFT(); renderBar();
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue