As-built: drop back view; inch dims on front+top (same width); TFT shows all lanes
- Removed the PCB back view (BOM stays in the right column). - Front and top views now share a left dimension gutter, so they're the same width and aligned, and carry inch dimensions: front 4.7 × 5.5 in (120 × 140 mm), top-edge thickness ≈ 1.8 in (45 mm), width 4.7 in below. - Reworked the TFT to use the empty space: tempo with the item name beside it, bar/beat at the bottom, and — filling the middle — ALL meter lanes drawn as rows of step pads (subdivisions, accent/normal/ghost/mute, amber beat ticks, per-lane playhead). Replaces the single beat-dots row. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9b5dfd63c5
commit
e8c613e3e4
1 changed files with 71 additions and 102 deletions
|
|
@ -10,8 +10,8 @@
|
||||||
parts you'd actually solder for an RP2040 build —
|
parts you'd actually solder for an RP2040 build —
|
||||||
• a 2.0″ 320×240 colour IPS TFT (ST7789, e.g. Pimoroni Pico Display 2.0):
|
• 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, smooth type,
|
the upgrade from the cramped 128×64 mono OLED — full colour, smooth type,
|
||||||
hi-DPI. It also draws the beat indicator itself — a row of beat dots with
|
hi-DPI. It also draws all lane patterns (each lane's steps — accent /
|
||||||
the current beat's subdivisions below — so there's no separate LED bar;
|
normal / ghost / mute — with the playhead), so there's no separate LED bar;
|
||||||
• controls: a big PLAY up top (a plain arcade button — it does NOT change
|
• controls: a big PLAY up top (a plain arcade button — it does NOT change
|
||||||
while playing; the screen shows transport state; an illuminated/RGB arcade
|
while playing; the screen shows transport state; an illuminated/RGB arcade
|
||||||
button could reflect it), then a row of PREV (far left), a recessed
|
button could reflect it), then a row of PREV (far left), a recessed
|
||||||
|
|
@ -24,8 +24,8 @@
|
||||||
amp + speaker. Powered from a standard 9 V DC pedal jack (2.1 mm centre-
|
amp + speaker. Powered from a standard 9 V DC pedal jack (2.1 mm centre-
|
||||||
negative, pedalboard-friendly) or USB-C (also carries config), in a brushed
|
negative, pedalboard-friendly) or USB-C (also carries config), in a brushed
|
||||||
aluminium / stainless enclosure.
|
aluminium / stainless enclosure.
|
||||||
Beside the device: a top-edge view, a back view (PCB + components) and a bill of
|
Beside the device: a top-edge view and a bill of materials. The front and top
|
||||||
materials. Overall ≈ 4.7 × 5.5 × 1.8 in (120 × 140 × 45 mm).
|
views carry inch dimensions (≈ 4.7 × 5.5 × 1.8 in / 120 × 140 × 45 mm).
|
||||||
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>
|
||||||
|
|
@ -97,7 +97,7 @@
|
||||||
/* side-by-side: player on the left, BOM on the right (stacks when narrow) */
|
/* side-by-side: player on the left, BOM on the right (stacks when narrow) */
|
||||||
.cols{ display:flex; flex-wrap:wrap; align-items:flex-start; justify-content:center; gap:18px; width:100% }
|
.cols{ display:flex; flex-wrap:wrap; align-items:flex-start; justify-content:center; gap:18px; width:100% }
|
||||||
.col-left{ display:flex; flex-direction:column; align-items:center; gap:14px; width:380px; max-width:100% }
|
.col-left{ display:flex; flex-direction:column; align-items:center; gap:14px; width:380px; max-width:100% }
|
||||||
.bom-panel{ width:100%; align-self:flex-start }
|
.bom-panel{ width:380px; max-width:100%; align-self:flex-start }
|
||||||
|
|
||||||
/* ---- top-edge view: all connectors on the top + total thickness ---- */
|
/* ---- top-edge view: all connectors on the top + total thickness ---- */
|
||||||
.topview{ width:380px; max-width:100%; display:flex; flex-direction:column; gap:5px }
|
.topview{ width:380px; max-width:100%; display:flex; flex-direction:column; gap:5px }
|
||||||
|
|
@ -118,27 +118,15 @@
|
||||||
.tv-dim{ display:flex; flex-direction:column; align-items:center; justify-content:center; font-size:9px; color:var(--muted); letter-spacing:.03em; line-height:1.05; white-space:nowrap }
|
.tv-dim{ display:flex; flex-direction:column; align-items:center; justify-content:center; font-size:9px; color:var(--muted); letter-spacing:.03em; line-height:1.05; white-space:nowrap }
|
||||||
.tv-dim .ar{ font-size:40px; line-height:.55; opacity:.5 }
|
.tv-dim .ar{ font-size:40px; line-height:.55; opacity:.5 }
|
||||||
|
|
||||||
/* ---- right column: back view (PCB) above the BOM ---- */
|
/* ---- dimensioned views: top edge + front share a left gutter, so they're the
|
||||||
.col-right{ display:flex; flex-direction:column; gap:14px; width:380px; max-width:100% }
|
same width and aligned; inch dims (thickness/height on the left, width below) ---- */
|
||||||
.backview{ width:100%; display:flex; flex-direction:column; gap:5px }
|
.dim-row{ display:flex; align-items:stretch; gap:6px; width:100% }
|
||||||
.bv-cap{ text-align:center; font-size:10px; color:var(--muted); letter-spacing:.02em }
|
.dim-y{ flex:0 0 13px; writing-mode:vertical-rl; transform:rotate(180deg);
|
||||||
.bv-frame{ display:flex; align-items:stretch; gap:6px }
|
display:flex; align-items:center; justify-content:center; text-align:center;
|
||||||
.bv-dimy{ writing-mode:vertical-rl; transform:rotate(180deg); text-align:center; font-size:9px; color:var(--muted);
|
font-size:8.5px; color:var(--muted); letter-spacing:.04em; white-space:nowrap; border-right:1px solid var(--panel-bd) }
|
||||||
letter-spacing:.05em; padding-right:3px; border-right:1px solid var(--panel-bd); white-space:nowrap }
|
.dim-row > .device, .dim-row > .tv-edge{ flex:1 1 0; min-width:0; max-width:none; width:auto }
|
||||||
.bv-dimx{ text-align:center; font-size:9px; color:var(--muted); letter-spacing:.05em; border-top:1px solid var(--panel-bd); padding-top:3px; margin:1px 0 0 20px }
|
.dim-x{ text-align:center; font-size:8.5px; color:var(--muted); letter-spacing:.04em;
|
||||||
.bv-board{ position:relative; flex:1; aspect-ratio:47/55; border-radius:6px; overflow:hidden;
|
border-top:1px solid var(--panel-bd); padding-top:3px; margin:3px 0 0 19px }
|
||||||
background:
|
|
||||||
repeating-linear-gradient(0deg, rgba(255,255,255,.022) 0 1px, transparent 1px 10px),
|
|
||||||
repeating-linear-gradient(90deg, rgba(255,255,255,.022) 0 1px, transparent 1px 10px),
|
|
||||||
linear-gradient(160deg, #134a3d, #0b3026);
|
|
||||||
border:1px solid #06231b; box-shadow:inset 0 0 16px rgba(0,0,0,.55), 0 12px 24px rgba(0,0,0,.4) }
|
|
||||||
.bv-mod{ position:absolute; display:flex; align-items:center; justify-content:center; text-align:center;
|
|
||||||
font-size:7px; font-weight:600; color:#d3ece2; letter-spacing:.02em; line-height:1.18; padding:2px;
|
|
||||||
background:rgba(7,26,20,.72); border:1px solid #2f6f5c; border-radius:3px }
|
|
||||||
.bv-rnd{ position:absolute; aspect-ratio:1; border-radius:50%; display:flex; align-items:center; justify-content:center;
|
|
||||||
font-size:6.5px; font-weight:700; color:#0a1e18; letter-spacing:.02em;
|
|
||||||
background:radial-gradient(circle at 38% 32%, #dbe3df, #8e9b95); border:1px solid #5f6b65; box-shadow:0 1px 2px rgba(0,0,0,.4) }
|
|
||||||
.bv-hole{ position:absolute; width:7px; height:7px; border-radius:50%; background:#05140f; border:1px solid #3a7e6a; box-shadow:inset 0 0 2px #000 }
|
|
||||||
|
|
||||||
/* ---- controls: encoder above (under the screen), arcade buttons spread below ----
|
/* ---- controls: encoder above (under the screen), arcade buttons spread below ----
|
||||||
wheel never hides the readout · PREV far-left / NEXT far-right · big central PLAY */
|
wheel never hides the readout · PREV far-left / NEXT far-right · big central PLAY */
|
||||||
|
|
@ -223,7 +211,8 @@
|
||||||
<!-- ===================== TOP EDGE (connectors) ===================== -->
|
<!-- ===================== TOP EDGE (connectors) ===================== -->
|
||||||
<div class="topview">
|
<div class="topview">
|
||||||
<div class="tv-cap">Top edge — all connectors (cables exit upward; pedalboard-friendly)</div>
|
<div class="tv-cap">Top edge — all connectors (cables exit upward; pedalboard-friendly)</div>
|
||||||
<div class="tv-row">
|
<div class="dim-row">
|
||||||
|
<div class="dim-y">↕ 1.8 in (45 mm)</div>
|
||||||
<div class="tv-edge">
|
<div class="tv-edge">
|
||||||
<div class="tv-jack" title="External trigger in — footswitch to start/stop or tap tempo"><i></i><b>Trig In</b></div>
|
<div class="tv-jack" title="External trigger in — footswitch to start/stop or tap tempo"><i></i><b>Trig In</b></div>
|
||||||
<div class="tv-jack" title="Instrument in — 1/4" pass-through; the click is mixed into your signal"><i></i><b>Inst In</b></div>
|
<div class="tv-jack" title="Instrument in — 1/4" pass-through; the click is mixed into your signal"><i></i><b>Inst In</b></div>
|
||||||
|
|
@ -231,12 +220,13 @@
|
||||||
<div class="tv-jack dc" title="9 V DC in — standard 2.1 mm centre-negative pedal power"><i></i><b>9V DC</b></div>
|
<div class="tv-jack dc" title="9 V DC in — standard 2.1 mm centre-negative pedal power"><i></i><b>9V DC</b></div>
|
||||||
<div class="tv-jack usb" title="USB-C — power & set-list transfer"><i></i><b>USB-C</b></div>
|
<div class="tv-jack usb" title="USB-C — power & set-list transfer"><i></i><b>USB-C</b></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tv-dim"><span class="ar">↕</span>≈ 1.8 in<br>(45 mm)</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="ledbar-cap">Trig in · 1/4″ inst pass‑through (click injected) · shared 1/4″ balanced‑TRS out · 9 V DC / USB‑C power</div>
|
<div class="ledbar-cap">Trig in · 1/4″ inst pass‑through (click injected) · shared 1/4″ balanced‑TRS out · 9 V DC / USB‑C power</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===================== THE DEVICE ===================== -->
|
<!-- ===================== THE DEVICE (front view) ===================== -->
|
||||||
|
<div class="dim-row">
|
||||||
|
<div class="dim-y">↕ 5.5 in (140 mm)</div>
|
||||||
<div class="device">
|
<div class="device">
|
||||||
|
|
||||||
<div class="brandrow">
|
<div class="brandrow">
|
||||||
|
|
@ -249,7 +239,7 @@
|
||||||
<canvas id="tft" width="320" height="240" aria-label="2 inch 320 by 240 colour IPS display"></canvas>
|
<canvas id="tft" width="320" height="240" aria-label="2 inch 320 by 240 colour IPS display"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tft-cap">2.0″ 320×240 IPS TFT (ST7789) — beat & subdivisions on‑screen</div>
|
<div class="tft-cap">2.0″ 320×240 IPS TFT (ST7789) — tempo, name & all lane patterns</div>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="key"><button class="abtn play" id="bPlay" title="play / stop (Space)">▶</button><small>Play / Stop</small></div>
|
<div class="key"><button class="abtn play" id="bPlay" title="play / stop (Space)">▶</button><small>Play / Stop</small></div>
|
||||||
|
|
@ -264,7 +254,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grille"></div>
|
<div class="grille"></div>
|
||||||
</div>
|
</div><!-- /device -->
|
||||||
|
</div><!-- /dim-row -->
|
||||||
|
<div class="dim-x">↔ 4.7 in (120 mm) wide</div>
|
||||||
|
|
||||||
<!-- ===================== LOAD CONFIG ===================== -->
|
<!-- ===================== LOAD CONFIG ===================== -->
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
|
|
@ -283,34 +275,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div><!-- /col-left -->
|
</div><!-- /col-left -->
|
||||||
|
|
||||||
<div class="col-right">
|
|
||||||
|
|
||||||
<!-- ===================== BACK VIEW (PCB + components) ===================== -->
|
|
||||||
<div class="backview">
|
|
||||||
<div class="bv-cap">Back view — PCB & components (mounting holes at the corners)</div>
|
|
||||||
<div class="bv-frame">
|
|
||||||
<div class="bv-dimy">↕ 5.5 in (140 mm)</div>
|
|
||||||
<div class="bv-board">
|
|
||||||
<span class="bv-hole" style="left:3%;top:2%"></span>
|
|
||||||
<span class="bv-hole" style="right:3%;top:2%"></span>
|
|
||||||
<span class="bv-hole" style="left:3%;bottom:2%"></span>
|
|
||||||
<span class="bv-hole" style="right:3%;bottom:2%"></span>
|
|
||||||
<div class="bv-mod" style="left:8%;top:2%;width:84%;height:6%">Trig · Inst · Out TRS · 9V DC · USB-C — top-edge jacks</div>
|
|
||||||
<div class="bv-mod" style="left:13%;top:11%;width:74%;height:24%">ST7789 2.0″ 320×240 TFT</div>
|
|
||||||
<div class="bv-mod" style="left:12%;top:38%;width:37%;height:9%">9 V→5 V buck</div>
|
|
||||||
<div class="bv-mod" style="left:53%;top:38%;width:35%;height:9%">RP2040</div>
|
|
||||||
<div class="bv-rnd" style="left:41%;top:50%;width:18%">PLAY</div>
|
|
||||||
<div class="bv-rnd" style="left:7%;top:65%;width:13%">PREV</div>
|
|
||||||
<div class="bv-rnd" style="left:35%;top:67%;width:9%">ENC</div>
|
|
||||||
<div class="bv-rnd" style="left:50%;top:65%;width:13%">TAP</div>
|
|
||||||
<div class="bv-rnd" style="right:7%;top:65%;width:13%">NEXT</div>
|
|
||||||
<div class="bv-mod" style="left:9%;top:80%;width:60%;height:13%">Analog audio — PCM5102 · NE5532 · DRV134 · PAM8302</div>
|
|
||||||
<div class="bv-rnd" style="right:8%;bottom:3%;width:19%">SPKR</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="bv-dimx">↔ 4.7 in (120 mm)</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ===================== BILL OF MATERIALS ===================== -->
|
<!-- ===================== BILL OF MATERIALS ===================== -->
|
||||||
<div class="panel bom-panel">
|
<div class="panel bom-panel">
|
||||||
<h2>Bill of materials</h2>
|
<h2>Bill of materials</h2>
|
||||||
|
|
@ -346,7 +310,6 @@
|
||||||
buffer of the 1/4″ instrument input, then fed to the balanced line driver (1/4″ TRS out) and the monitor amp —
|
buffer of the 1/4″ instrument input, then fed to the balanced line driver (1/4″ TRS out) and the monitor amp —
|
||||||
so your instrument is never re‑digitised (no added latency).</p>
|
so your instrument is never re‑digitised (no added latency).</p>
|
||||||
</div>
|
</div>
|
||||||
</div><!-- /col-right -->
|
|
||||||
</div><!-- /cols -->
|
</div><!-- /cols -->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -436,57 +399,63 @@ const TFT_W=320, TFT_H=240;
|
||||||
const tft=$("tft"), tc=tft.getContext("2d");
|
const tft=$("tft"), tc=tft.getContext("2d");
|
||||||
(function(){ const dpr=Math.max(1,Math.min(3,window.devicePixelRatio||1));
|
(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
|
tft.width=TFT_W*dpr; tft.height=TFT_H*dpr; tc.scale(dpr,dpr); })(); // hi-DPI backing → crisp type
|
||||||
function drawTFT(){
|
const SND_ABBR={kick:"KICK",snare:"SNR",rim:"RIM",clap:"CLAP",hatClosed:"HAT",hatOpen:"OHAT",ride:"RIDE",crash:"CRSH",
|
||||||
const ref=meters[0], bpb=ref?ref.beatsPerBar:0;
|
tomLow:"TOM↓",tomMid:"TOM",tomHigh:"TOM↑",tambourine:"TAMB",cowbell:"CBL",woodblock:"WOOD",claves:"CLAV",jamblock:"JAM",beep:"BEEP"};
|
||||||
// beats from lane 1; subdivisions from the finest lane sharing that beat grid
|
function soundLabel(s){ if(SND_ABBR[s]) return SND_ABBR[s];
|
||||||
let disp=ref; if(ref){ for(const x of meters){ if(!x.poly && x.beatsPerBar===ref.beatsPerBar && x.stepsPerBeat>disp.stepsPerBeat) disp=x; } }
|
const m=String(s).match(/^([a-zA-Z]+)(\d+)?$/);
|
||||||
const spb=disp?disp.stepsPerBeat:1;
|
return m ? (m[1].slice(0,3).toUpperCase()+(m[2]||"")) : String(s).slice(0,5).toUpperCase(); }
|
||||||
let curBeat=-1, curSub=-1;
|
|
||||||
if(state.running&&disp&&disp.currentStep>=0){ curBeat=Math.floor(disp.currentStep/spb); curSub=disp.currentStep%spb; }
|
|
||||||
|
|
||||||
|
function drawTFT(){
|
||||||
|
const ref=meters[0];
|
||||||
const g=tc.createLinearGradient(0,0,0,TFT_H); g.addColorStop(0,"#0b0f18"); g.addColorStop(1,"#05070c");
|
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);
|
tc.fillStyle=g; tc.fillRect(0,0,TFT_W,TFT_H);
|
||||||
// header
|
// header
|
||||||
tc.textBaseline="middle"; tc.textAlign="left";
|
tc.textBaseline="middle"; tc.textAlign="left";
|
||||||
tc.font='600 13px "Segoe UI",system-ui,sans-serif'; tc.fillStyle="#5b7a93";
|
tc.font='600 12px "Segoe UI",system-ui,sans-serif'; tc.fillStyle="#5b7a93";
|
||||||
tc.fillText("♪ "+(setlist?((idx+1)+"/"+setlist.items.length):"–/–"), 14, 17);
|
tc.fillText("♪ "+(setlist?((idx+1)+"/"+setlist.items.length):"–/–"), 12, 13);
|
||||||
tc.textAlign="right"; tc.fillStyle = state.running ? "#2fe07a" : "#6b7787";
|
tc.textAlign="right"; tc.fillStyle = state.running ? "#2fe07a" : "#6b7787";
|
||||||
tc.fillText(state.running?"▶ PLAY":"■ STOP", TFT_W-14, 17);
|
tc.fillText(state.running?"▶ PLAY":"■ STOP", TFT_W-12, 13);
|
||||||
tc.fillStyle="#13283c"; tc.fillRect(14,30,TFT_W-28,1);
|
tc.fillStyle="#13283c"; tc.fillRect(12,25,TFT_W-24,1);
|
||||||
// tempo — the hero number
|
// tempo (left) + name (right) — uses the wide upper area
|
||||||
tc.textBaseline="alphabetic"; tc.textAlign="left";
|
tc.textBaseline="alphabetic"; tc.textAlign="left";
|
||||||
tc.fillStyle="#1fb6f0"; tc.font='800 78px "Segoe UI",system-ui,sans-serif';
|
tc.fillStyle="#1fb6f0"; tc.font='800 50px "Segoe UI",system-ui,sans-serif';
|
||||||
const bpm=String(state.bpm); tc.fillText(bpm, 16, 110);
|
const bpm=String(state.bpm); tc.fillText(bpm, 12, 74);
|
||||||
const bx=16+tc.measureText(bpm).width+9;
|
const bx=12+tc.measureText(bpm).width+8;
|
||||||
tc.fillStyle="#5b7a93"; tc.font='700 16px "Segoe UI",system-ui,sans-serif'; tc.fillText("BPM", bx, 74);
|
tc.fillStyle="#5b7a93"; tc.font='700 13px "Segoe UI",system-ui,sans-serif'; tc.fillText("BPM", bx, 48);
|
||||||
tc.font='600 17px "Segoe UI",system-ui,sans-serif'; tc.fillText("♩ "+(ref?(ref.groupsStr.replace(/[^0-9+]/g,"")||ref.beatsPerBar):"–"), bx, 98);
|
tc.font='600 13px "Segoe UI",system-ui,sans-serif'; tc.fillText("♩ "+(ref?(ref.groupsStr.replace(/[^0-9+]/g,"")||ref.beatsPerBar):"–"), bx, 67);
|
||||||
// item name — centred, ellipsised
|
// item name, right-aligned, ellipsised to the space right of the tempo labels
|
||||||
tc.textAlign="center"; tc.fillStyle="#e9eff7"; tc.font='600 22px "Segoe UI",system-ui,sans-serif';
|
tc.textAlign="right"; tc.fillStyle="#e9eff7"; tc.font='600 16px "Segoe UI",system-ui,sans-serif';
|
||||||
let nm=setlist?(setlist.items[idx].name||"—"):"—";
|
let nm=setlist?(setlist.items[idx].name||"—"):"—"; const nmMax=(TFT_W-12)-(bx+42);
|
||||||
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+="…"; }
|
if(tc.measureText(nm).width>nmMax){ while(nm.length>1 && tc.measureText(nm+"…").width>nmMax) nm=nm.slice(0,-1); nm+="…"; }
|
||||||
tc.fillText(nm, TFT_W/2, 138);
|
tc.fillText(nm, TFT_W-12, 58);
|
||||||
// beat dots + the current beat's subdivisions — the on-screen replacement for the LED matrix
|
tc.fillStyle="#13283c"; tc.fillRect(12,84,TFT_W-24,1);
|
||||||
if(bpb>0){
|
// ---- all meter lanes: each a row of step pads (subdivisions · accent/normal/ghost/mute · playhead) ----
|
||||||
const gap=Math.min(30,(TFT_W-44)/bpb), x0=TFT_W/2 - gap*(bpb-1)/2, by=166;
|
const lanes=meters||[], gx0=48, gx1=TFT_W-12, gw=gx1-gx0, top=90, bot=204;
|
||||||
for(let i=0;i<bpb;i++){
|
const rowH = lanes.length ? Math.min(20,(bot-top)/lanes.length) : 0;
|
||||||
const x=x0+i*gap, grp=ref.groupStarts.has(i), on=(i===curBeat), col=grp?"#ffd166":"#1fb6f0";
|
tc.textBaseline="middle";
|
||||||
tc.beginPath(); tc.arc(x,by, on?7:4, 0, Math.PI*2);
|
lanes.forEach((m,li)=>{
|
||||||
if(on){ tc.fillStyle=col; tc.shadowColor=col; tc.shadowBlur=11; tc.fill(); tc.shadowBlur=0; }
|
const cy=top+li*rowH+rowH/2, en=m.enabled;
|
||||||
else { tc.fillStyle=grp?"rgba(255,209,102,.34)":"rgba(31,182,240,.26)"; tc.fill(); }
|
tc.textAlign="left"; tc.font='700 8px "Segoe UI",system-ui,sans-serif'; tc.fillStyle=en?"#9fb4c4":"#4a5560";
|
||||||
}
|
tc.fillText(soundLabel(m.sound), 6, cy+0.5);
|
||||||
if(spb>1){
|
const nsteps=Math.max(1,m.beatsPerBar*m.stepsPerBeat), cw=gw/nsteps, ch=Math.min(rowH-4,13), cyTop=cy-ch/2;
|
||||||
const sgap=8, sx0=TFT_W/2 - sgap*(spb-1)/2, sy=188;
|
for(let s=0;s<nsteps;s++){
|
||||||
for(let s=0;s<spb;s++){ tc.beginPath(); tc.arc(sx0+s*sgap, sy, 2.4, 0, Math.PI*2);
|
const cx=gx0+s*cw, lvl=m.beatsOn[s]|0, beatStart=(s%m.stepsPerBeat)===0, cur=state.running&&m.currentStep===s;
|
||||||
tc.fillStyle=(curBeat>=0&&s<=curSub)?"#2fe0a0":"rgba(47,224,160,.16)"; tc.fill(); }
|
const x=cx+0.6, w=Math.max(1,cw-1.4);
|
||||||
}
|
let fill = !en ? (lvl?"rgba(120,140,160,.18)":null)
|
||||||
|
: lvl===2?"#1fb6f0" : lvl===1?"rgba(31,182,240,.62)" : lvl===3?"rgba(31,182,240,.30)" : null;
|
||||||
|
if(fill){ tc.fillStyle=fill; tc.fillRect(x,cyTop,w,ch); }
|
||||||
|
else { tc.strokeStyle="rgba(120,140,160,.22)"; tc.lineWidth=1; tc.strokeRect(x+0.5,cyTop+0.5,w-1,ch-1); } // mute = outline
|
||||||
|
if(beatStart){ tc.fillStyle="rgba(255,209,102,.55)"; tc.fillRect(cx,cyTop-1,1,ch+2); } // beat tick
|
||||||
|
if(cur){ tc.strokeStyle="#fff"; tc.lineWidth=1.4; tc.strokeRect(x-0.6,cyTop-0.6,w+1.2,ch+1.2); } // playhead
|
||||||
}
|
}
|
||||||
|
});
|
||||||
// bottom strip
|
// bottom strip
|
||||||
tc.fillStyle="#13283c"; tc.fillRect(14,206,TFT_W-28,1);
|
tc.fillStyle="#13283c"; tc.fillRect(12,208,TFT_W-24,1);
|
||||||
tc.textBaseline="alphabetic"; tc.textAlign="left"; tc.fillStyle="#7f8b9a"; tc.font='600 14px "Segoe UI",system-ui,sans-serif';
|
tc.textBaseline="alphabetic"; tc.textAlign="left"; tc.fillStyle="#7f8b9a"; tc.font='600 13px "Segoe UI",system-ui,sans-serif';
|
||||||
tc.fillText(state.running&&ref ? ("BAR "+(ref.currentBar+1)) : "READY", 14, 226);
|
tc.fillText(state.running&&ref ? ("BAR "+(ref.currentBar+1)+" · BEAT "+(Math.floor(ref.currentStep/ref.stepsPerBeat)+1||"–")) : "READY", 12, 226);
|
||||||
if(segBars>0){ const rem=Math.max(0,segBars-(ref?ref.currentBar:0));
|
if(segBars>0){ const rem=Math.max(0,segBars-(ref?ref.currentBar:0));
|
||||||
tc.textAlign="right"; tc.fillStyle="#ffd166"; tc.font='700 15px "Segoe UI",system-ui,sans-serif';
|
tc.textAlign="right"; tc.fillStyle="#ffd166"; tc.font='700 14px "Segoe UI",system-ui,sans-serif';
|
||||||
tc.fillText((state.running?rem:segBars)+" BARS", TFT_W-14, 226); }
|
tc.fillText((state.running?rem:segBars)+" BARS", TFT_W-12, 226); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAll(){ drawTFT(); /* PLAY button is static hardware — transport state is shown on the screen */
|
function renderAll(){ drawTFT(); /* PLAY button is static hardware — transport state is shown on the screen */
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue