A new, first-actually-buildable form factor for the 52Pi EP-0172 "Pico Breadboard
Kit Plus" (Raspberry Pi Pico; 3.5" ST7796 320x480 cap-touch via GT911, PSP
joystick on ADC0/1, WS2812 RGB on GP12, buzzer GP13, buttons GP14/15):
- pico/main.py — one self-contained MicroPython file: ST7796 direct-draw driver,
GT911 touch (16-bit register addressing), WS2812 RGB (neopixel), PWM buzzer,
ADC joystick, buttons. It parses the project's own program-string language
(verified against the web engine's semantics) and runs a non-blocking
ticks_us scheduler with an on-screen touch UI. CONFIG flags cover panel /
colour / touch / joystick calibration. pico/README.md has flashing +
calibration steps.
- kit.html — lean widget that mirrors the firmware's on-screen UI (portrait
320x480 canvas) plus a joystick / RGB / buzzer / A-B buttons; plays via the
shared engine. info-kit.html — the real EP-0172 pinout, a parts list
(~$45 incl. Pico) and the firmware to flash (downloads /pico-main.py, links
the README + source).
- Landing + embed page list the Kit; build.sh/deploy.sh build the two pages and
serve pico/main.py as /pico-main.py for download.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
308 lines
17 KiB
HTML
308 lines
17 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||
<title>VARASYS PM_K‑1 — Kit (Raspberry Pi Pico touchscreen build)</title>
|
||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,@BUILD:favicon@">
|
||
<script>
|
||
/* ?embed=1 → strip site chrome + auto-size to the host */
|
||
(function(){ if(!/[?&]embed=1/.test(location.search)) return;
|
||
document.documentElement.dataset.embed="1";
|
||
function ph(){ try{ parent.postMessage({type:"varasys-h",h:Math.ceil(document.documentElement.getBoundingClientRect().height)},"*"); }catch(e){} }
|
||
addEventListener("load",ph); addEventListener("resize",ph); setTimeout(ph,300); setTimeout(ph,1000);
|
||
})();
|
||
</script>
|
||
<!--
|
||
PM_K-1 "Kit" — the buildable touchscreen unit: a Raspberry Pi Pico on the 52Pi EP-0172
|
||
breadboard kit (3.5" ST7796 320x480 cap-touch, GT911 touch, PSP joystick on ADC0/1,
|
||
WS2812 RGB on GP12, buzzer GP13, buttons GP14/15). This page mirrors the MicroPython
|
||
firmware's on-screen UI so the web simulator looks like the real device. Shares src/engine.js.
|
||
-->
|
||
<script>
|
||
(function(){ try{ var p = localStorage.getItem("metronome.theme");
|
||
if (p!=="light" && p!=="dark" && p!=="system") p = "system";
|
||
document.documentElement.dataset.theme = p==="system" ? (matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark") : p;
|
||
} catch(e){ document.documentElement.dataset.theme = "dark"; } })();
|
||
</script>
|
||
<style>
|
||
/*@BUILD:include:src/base.css@*/
|
||
:root{ --bg1:#12151c; --bg2:#05070a; --txt:#c7d0db; --muted:#7f8b9a; --link:#6cb6ff;
|
||
--panel-bd:#2a313c; --device-bd:#33363c; --silk:#aab2bc; --cyan:#0AB3F7;
|
||
--panel-bg:#161b22; --field-bg:#0e1116; --field-bd:#2a313c; }
|
||
:root[data-theme="light"]{ --bg1:#f5f8fc; --bg2:#dde4ec; --txt:#1e2630; --muted:#5c6776; --link:#1769c4;
|
||
--panel-bd:#d2dae4; --panel-bg:#ffffff; --field-bg:#f1f4f8; --field-bd:#d2dae4 }
|
||
body{ margin:0; min-height:100vh; padding:26px 14px 46px;
|
||
background:radial-gradient(circle at 50% -8%, var(--bg1), var(--bg2)); color:var(--txt);
|
||
display:flex; flex-direction:column; align-items:center; gap:16px }
|
||
a{ color:var(--link) }
|
||
|
||
/* the kit: a Pico carrier board with the screen + joystick + RGB + buzzer + buttons */
|
||
.device{ width:100%; max-width:330px; position:relative; border-radius:16px; padding:14px 14px 18px;
|
||
background:
|
||
radial-gradient(rgba(255,255,255,.022) .5px, transparent .6px) 0 0/3px 3px,
|
||
linear-gradient(180deg, #1f3a2e, #0c2a20); /* 52Pi green PCB vibe */
|
||
border:1px solid #2d5c47;
|
||
box-shadow:0 26px 52px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.05) }
|
||
.pcbrow{ display:flex; align-items:center; justify-content:space-between; margin:0 2px 10px }
|
||
.dev-logo{ height:18px }
|
||
.silk{ display:flex; align-items:center; gap:7px; color:#bfe6d4 }
|
||
.silk .model{ font-size:8.5px; text-transform:uppercase; letter-spacing:.16em; opacity:.9 }
|
||
.pin{ font-size:7px; color:#9fd2bd; letter-spacing:.1em; text-transform:uppercase; opacity:.8 }
|
||
|
||
.screen-wrap{ padding:8px; border-radius:10px; background:linear-gradient(180deg,#05070a,#020406);
|
||
border:1px solid #04060a; box-shadow:inset 0 2px 10px rgba(0,0,0,.8) }
|
||
#screen{ display:block; width:100%; height:auto; border-radius:5px; background:#06080c; touch-action:manipulation; cursor:pointer }
|
||
|
||
.hw{ display:flex; align-items:center; justify-content:space-between; gap:10px; margin:14px 4px 0 }
|
||
/* PSP joystick */
|
||
.joy{ width:74px; height:74px; border-radius:50%; position:relative; flex:0 0 auto; touch-action:none; cursor:grab;
|
||
background:radial-gradient(circle at 40% 34%, #2a2f37, #0c0f13 72%); border:2px solid #0a3a2a;
|
||
box-shadow:inset 0 2px 6px rgba(0,0,0,.6) }
|
||
.joy .nub{ position:absolute; left:50%; top:50%; width:34px; height:34px; margin:-17px 0 0 -17px; border-radius:50%;
|
||
background:radial-gradient(circle at 38% 32%, #e9eef3, #aab2bc 46%, #6c7480 72%, #3b424c);
|
||
box-shadow:0 3px 6px rgba(0,0,0,.5); transition:transform .04s }
|
||
.joy .cap{ position:absolute; left:0; right:0; bottom:-15px; text-align:center; font-size:7px; color:#9fd2bd; letter-spacing:.08em; text-transform:uppercase; opacity:.85 }
|
||
.mids{ display:flex; flex-direction:column; align-items:center; gap:8px; flex:1 }
|
||
.led{ width:30px; height:30px; border-radius:50%; background:#0c0f14;
|
||
box-shadow:0 0 4px #000 inset; transition:none }
|
||
.led-cap, .buz-cap{ font-size:7px; color:#9fd2bd; letter-spacing:.1em; text-transform:uppercase; opacity:.85 }
|
||
.buz{ width:26px; height:26px; border-radius:50%; background:radial-gradient(circle at 50% 40%, #2a2f37, #0c0f13);
|
||
border:2px solid #0a3a2a; position:relative }
|
||
.buz::after{ content:""; position:absolute; left:50%; top:50%; width:6px; height:6px; margin:-3px 0 0 -3px; border-radius:50%; background:#05070a }
|
||
.btns{ display:flex; flex-direction:column; gap:9px; flex:0 0 auto }
|
||
.pbtn{ width:60px; padding:9px 0; border-radius:9px; border:1px solid #0a3a2a; cursor:pointer;
|
||
background:radial-gradient(circle at 40% 30%, #d7dde3, #8b939e 70%, #5a626c); color:#0c1116;
|
||
font-size:9px; font-weight:800; letter-spacing:.06em; text-transform:uppercase;
|
||
box-shadow:0 3px 5px rgba(0,0,0,.4) }
|
||
.pbtn:active{ transform:translateY(2px) }
|
||
.pbtn small{ display:block; font-size:6.5px; font-weight:600; opacity:.7 }
|
||
|
||
.hint{ max-width:330px; text-align:center; font-size:11px; color:var(--muted); line-height:1.5 }
|
||
[data-embed] .hint{ display:none !important }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
/*@BUILD:include:src/header.html@*/
|
||
|
||
<h1 class="ff-title">PM_K‑1 Kit</h1>
|
||
<p class="ff-sum">The build‑it‑yourself touchscreen unit — a Raspberry Pi Pico on the 52Pi breadboard kit (3.5″ cap‑touch, joystick, RGB, buzzer). Tap the screen, nudge tempo with the stick; runs the same program strings, with MicroPython firmware you flash yourself.</p>
|
||
|
||
<div class="device">
|
||
<div class="pcbrow">
|
||
<div class="silk"><span class="dev-lock"><img class="dev-logo" src="data:image/png;base64,@BUILD:logo-dark@" alt="VARASYS — Simplifying Complexity" /></span><span class="model">PM_K‑1 Kit</span></div>
|
||
<span class="pin">Pico · USB‑C</span>
|
||
</div>
|
||
|
||
<div class="screen-wrap"><canvas id="screen" width="320" height="480" aria-label="touchscreen metronome"></canvas></div>
|
||
|
||
<div class="hw">
|
||
<div class="joy" id="joy" title="Joystick — up/down tempo · left/right groove"><div class="nub" id="nub"></div><span class="cap">Joystick</span></div>
|
||
<div class="mids">
|
||
<div class="led" id="led"></div><span class="led-cap">RGB</span>
|
||
<div class="buz"></div><span class="buz-cap">Buzzer</span>
|
||
</div>
|
||
<div class="btns">
|
||
<button class="pbtn" id="btnA">A<small>play</small></button>
|
||
<button class="pbtn" id="btnB">B<small>tap</small></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="hint">Tap the on‑screen buttons (the real unit is capacitive touch). The <b>joystick</b> sets tempo (up/down)
|
||
and switches grooves (left/right); <b>A</b> = play/stop, <b>B</b> = tap. The RGB LED flashes each beat and the buzzer clicks.</div>
|
||
|
||
/*@BUILD:include:src/progbox.html@*/
|
||
|
||
<p class="ff-link pageonly"><a href="/info-kit.html">Wiring, parts & firmware to flash →</a></p>
|
||
|
||
<script>
|
||
const APP_VERSION = "v0.0.1-dev";
|
||
const $ = (id) => document.getElementById(id);
|
||
|
||
/* ========================= ENGINE (shared; synth voices only) ================= */
|
||
const SAMPLES = {};
|
||
/*@BUILD:include:src/engine.js@*/
|
||
/*@BUILD:include:src/setlists.js@*/
|
||
const state = { bpm:120, volume:0.85, running:false };
|
||
let meters = [], muteWindows = [];
|
||
|
||
function setBpm(v){ state.bpm = Math.max(30, Math.min(300, Math.round(v))); if(window.progRefresh) progRefresh(); }
|
||
function scheduler(){
|
||
const ahead = audioCtx.currentTime + SCHEDULE_AHEAD;
|
||
for(const m of meters){ while(m.nextTime < ahead){ scheduleMeterTick(m, m.nextTime); m.nextTime += laneStepDur(m, m.tick); m.tick++; } }
|
||
}
|
||
function buildMeters(lanes){
|
||
return (lanes||[]).map(c=>{ const p=parseGroups(c.groupsStr);
|
||
return {groupsStr:c.groupsStr,groups:p.groups,beatsPerBar:p.beatsPerBar,groupStarts:p.groupStarts,
|
||
stepsPerBeat:c.stepsPerBeat||1,sound:c.sound,beatsOn:(c.beatsOn||[]).slice(),poly:!!c.poly,swing:!!c.swing,enabled:c.enabled!==false,gainDb:c.gainDb||0,
|
||
tick:0,nextTime:0,vq:[],vqPtr:0,currentStep:-1,currentBar:0}; });
|
||
}
|
||
function startAudio(){
|
||
ensureAudio(); audioCtx.resume(); state.running=true;
|
||
const t0=audioCtx.currentTime+0.08;
|
||
for(const m of meters){ m.tick=0; m.nextTime=t0; m.vq=[]; m.vqPtr=0; m.currentStep=-1; }
|
||
muteWindows=[];
|
||
schedulerTimer=setInterval(scheduler,LOOKAHEAD_MS); scheduler();
|
||
}
|
||
function stopAudio(){ state.running=false; clearInterval(schedulerTimer); schedulerTimer=null; for(const m of meters) m.currentStep=-1; }
|
||
function toggle(){ state.running ? stopAudio() : startAudio(); }
|
||
|
||
/* ========================= TRACKS (seed grooves, flattened) =================== */
|
||
let tracks = SEED_SETLISTS.flatMap(sl => sl.items.map(([n,p]) => ({ name:n, ...patchToSetup(p) })));
|
||
let trackIdx = 0;
|
||
function tracksFromHash(){
|
||
const m=(location.hash||"").match(/[#&](p|sl)=([^&]+)/); if(!m) return null;
|
||
let payload=m[2]; try{ payload=decodeURIComponent(payload); }catch(e){}
|
||
try{
|
||
if(m[1]==="sl"){ const sl=codeToSetlist(payload); return sl.items.length ? sl.items.map(it=>({...it})) : null; }
|
||
const s=patchToSetup(payload); return s.lanes.length ? [{name:"Patch",...s}] : null;
|
||
}catch(e){ return null; }
|
||
}
|
||
function loadTrack(i){
|
||
const n=tracks.length; if(!n) return; trackIdx=((i%n)+n)%n;
|
||
const t=tracks[trackIdx]; setBpm(t.bpm||120); meters=buildMeters(t.lanes);
|
||
const was=state.running; if(was){ clearInterval(schedulerTimer); schedulerTimer=null; state.running=false; }
|
||
if(was) startAudio();
|
||
}
|
||
|
||
/* ========================= SCREEN (canvas mirrors the firmware UI) ============ */
|
||
const cv=$("screen"), g=cv.getContext("2d"), SW=320, SH=480;
|
||
(function(){ const dpr=Math.max(1,Math.min(3,window.devicePixelRatio||1)); cv.width=SW*dpr; cv.height=SH*dpr; g.scale(dpr,dpr); })();
|
||
const COL={ bg:"#06090e", txt:"#c7d0db", mute:"#6e7a8a", cyan:"#0AB3F7", amber:"#ff9b2e",
|
||
violet:"#967bff", green:"#2fe07a", dim:"#243240", btn:"#1c222c", panel:"#12161e" };
|
||
const PRIO={2:3,1:2,3:1};
|
||
const LEDCOL={2:[255,110,0],1:[0,150,255],3:[130,70,255]};
|
||
let flash=0, flashLevel=1, beatIdx=-1, btnRects=[];
|
||
|
||
function rrect(x,y,w,h,r){ g.beginPath(); g.moveTo(x+r,y); g.arcTo(x+w,y,x+w,y+h,r); g.arcTo(x+w,y+h,x,y+h,r); g.arcTo(x,y+h,x,y,r); g.arcTo(x,y,x+w,y,r); g.closePath(); }
|
||
|
||
function layoutButtons(){
|
||
btnRects=[]; const bw=96, bh=54, gap=(SW-3*bw)/4, xs=[gap, gap*2+bw, gap*3+bw*2];
|
||
[["prev",300],["play",300],["next",300]].forEach((b,i)=>btnRects.push({x:xs[i],y:300,w:bw,h:bh,key:["prev","play","next"][i]}));
|
||
["minus","tap","plus"].forEach((k,i)=>btnRects.push({x:xs[i],y:370,w:bw,h:bh,key:k}));
|
||
}
|
||
layoutButtons();
|
||
|
||
function drawScreen(){
|
||
g.fillStyle=COL.bg; g.fillRect(0,0,SW,SH);
|
||
// header
|
||
g.textBaseline="alphabetic"; g.textAlign="left";
|
||
g.fillStyle=COL.cyan; g.font="700 18px 'Segoe UI',Roboto,Arial,sans-serif"; g.fillText("VARASYS",12,26);
|
||
g.fillStyle=COL.mute; g.font="700 11px 'Segoe UI',Roboto,Arial,sans-serif"; g.textAlign="right"; g.fillText("PM_K‑1 KIT",SW-12,24);
|
||
g.fillStyle=COL.panel; g.fillRect(0,34,SW,2);
|
||
// BPM
|
||
g.textAlign="left"; g.fillStyle=COL.mute; g.font="700 20px 'Segoe UI',Roboto,Arial,sans-serif"; g.fillText("BPM",12,150);
|
||
g.textAlign="right"; g.fillStyle=COL.txt; g.font="800 92px 'Segoe UI',Roboto,Arial,sans-serif"; g.fillText(String(state.bpm),SW-12,176);
|
||
// beat dots
|
||
const m=meters[0]; if(m){ const bpb=m.beatsPerBar, sz=18, sp=26, x0=Math.max(12,SW-12-bpb*sp), y=196;
|
||
for(let i=0;i<bpb;i++){ const accent=m.groupStarts.has(i)||(m.beatsOn[i*m.stepsPerBeat]|0)>=2;
|
||
const on=state.running && i===beatIdx; g.fillStyle = on ? (accent?COL.amber:COL.cyan) : COL.dim;
|
||
g.fillRect(x0+i*sp,y,sz,sz); } }
|
||
// status
|
||
g.textAlign="left"; g.fillStyle=state.running?COL.green:COL.mute; g.font="700 16px 'Segoe UI',Roboto,Arial,sans-serif";
|
||
g.fillText(state.running?"▶ RUN":"■ STOP",12,256);
|
||
const nm=(tracks[trackIdx]&&tracks[trackIdx].name||"—").slice(0,18);
|
||
g.textAlign="right"; g.fillStyle=COL.txt; g.fillText(nm,SW-12,256);
|
||
g.textAlign="left"; g.fillStyle=COL.mute; g.font="600 11px 'Segoe UI',Roboto,Arial,sans-serif"; g.fillText((trackIdx+1)+"/"+tracks.length,12,276);
|
||
// touch buttons
|
||
const lbl={prev:"‹‹",play:state.running?"▮▮":"▶",next:"››",minus:"–",tap:"TAP",plus:"+"};
|
||
for(const b of btnRects){ g.fillStyle=COL.btn; rrect(b.x,b.y,b.w,b.h,9); g.fill();
|
||
g.fillStyle = b.key==="play" ? COL.green : COL.txt;
|
||
g.font = (b.key==="minus"||b.key==="plus") ? "800 30px 'Segoe UI',Roboto,Arial,sans-serif" : "800 22px 'Segoe UI',Roboto,Arial,sans-serif";
|
||
g.textAlign="center"; g.textBaseline="middle"; g.fillText(lbl[b.key], b.x+b.w/2, b.y+b.h/2+2); g.textBaseline="alphabetic"; }
|
||
g.textAlign="left"; g.fillStyle=COL.mute; g.font="600 10px 'Segoe UI',Roboto,Arial,sans-serif";
|
||
g.fillText("joystick: tempo / groove · A play B tap", 12, SH-14);
|
||
}
|
||
|
||
function audioLatency(){ return audioCtx ? (audioCtx.outputLatency || audioCtx.baseLatency || 0) : 0; }
|
||
function frame(){
|
||
const now = audioCtx ? audioCtx.currentTime - audioLatency() : 0;
|
||
if(audioCtx && state.running){
|
||
let fired=[];
|
||
for(const m of meters){
|
||
while(m.vqPtr<m.vq.length && m.vq[m.vqPtr].time<=now){
|
||
const e=m.vq[m.vqPtr]; m.currentStep=e.step; m.currentBar=e.bar;
|
||
const lvl=m.beatsOn[e.step]|0; if(lvl>0) fired.push(lvl);
|
||
if(m===meters[0] && e.step % m.stepsPerBeat===0) beatIdx = e.step/m.stepsPerBeat;
|
||
m.vqPtr++;
|
||
}
|
||
if(m.vqPtr>512){ m.vq=m.vq.slice(m.vqPtr); m.vqPtr=0; }
|
||
}
|
||
if(fired.length){ flashLevel = fired.sort((a,b)=>PRIO[b]-PRIO[a])[0]; flash=1; }
|
||
}
|
||
flash=Math.max(0,flash-0.08);
|
||
// RGB LED element
|
||
const c=LEDCOL[flashLevel]||LEDCOL[1], lit=flash;
|
||
const led=$("led");
|
||
if(lit>0.02){ const rgb="rgb("+c.map(v=>Math.round(v*lit)).join(",")+")";
|
||
led.style.background=rgb; led.style.boxShadow="0 0 "+(8+lit*22)+"px rgb("+c.join(",")+")"; }
|
||
else { led.style.background="#0c0f14"; led.style.boxShadow="0 0 4px #000 inset"; }
|
||
drawScreen();
|
||
requestAnimationFrame(frame);
|
||
}
|
||
|
||
/* ========================= INPUTS ============================================ */
|
||
function dispatch(key){
|
||
if(key==="play") toggle();
|
||
else if(key==="prev") loadTrack(trackIdx-1);
|
||
else if(key==="next") loadTrack(trackIdx+1);
|
||
else if(key==="minus") setBpm(state.bpm-1);
|
||
else if(key==="plus") setBpm(state.bpm+1);
|
||
else if(key==="tap") tapTempo();
|
||
}
|
||
cv.addEventListener("pointerdown",(e)=>{
|
||
const r=cv.getBoundingClientRect(); const x=(e.clientX-r.left)*SW/r.width, y=(e.clientY-r.top)*SH/r.height;
|
||
for(const b of btnRects){ if(x>=b.x&&x<=b.x+b.w&&y>=b.y&&y<=b.y+b.h){ dispatch(b.key); return; } }
|
||
});
|
||
|
||
let taps=[];
|
||
function tapTempo(){ const now=performance.now(); taps.push(now); taps=taps.filter(x=>now-x<2400);
|
||
if(taps.length>=2){ let s=0; for(let i=1;i<taps.length;i++) s+=taps[i]-taps[i-1];
|
||
const bpm=Math.round(60000/(s/(taps.length-1))); if(bpm>=30&&bpm<=300) setBpm(bpm); } }
|
||
$("btnA").addEventListener("click",()=>toggle());
|
||
$("btnB").addEventListener("click",()=>tapTempo());
|
||
|
||
/* joystick: drag up/down = tempo, left/right = groove */
|
||
(function(){
|
||
const joy=$("joy"), nub=$("nub"); let dragging=false, R=26, last=0, itemLatch=0;
|
||
function at(e){ const r=joy.getBoundingClientRect(); return {x:e.clientX-r.left-r.width/2, y:e.clientY-r.top-r.height/2}; }
|
||
function move(e){ if(!dragging) return; e.preventDefault();
|
||
let {x,y}=at(e); const d=Math.hypot(x,y)||1, k=Math.min(1,R/d); let nx=x*k, ny=y*k;
|
||
nub.style.transform="translate("+nx+"px,"+ny+"px)";
|
||
const fy=-ny/R, fx=nx/R, now=performance.now();
|
||
if(Math.abs(fy)>0.45 && now-last>80){ setBpm(state.bpm+(fy>0?1:-1)*(Math.abs(fy)>0.8?5:1)); last=now; }
|
||
if(Math.abs(fx)>0.6){ if(now-itemLatch>320){ loadTrack(trackIdx+(fx>0?1:-1)); itemLatch=now; } }
|
||
}
|
||
function up(){ dragging=false; nub.style.transform="translate(0,0)"; }
|
||
joy.addEventListener("pointerdown",(e)=>{ dragging=true; try{joy.setPointerCapture(e.pointerId);}catch(_){ } move(e); });
|
||
joy.addEventListener("pointermove",move);
|
||
joy.addEventListener("pointerup",up); joy.addEventListener("pointercancel",up);
|
||
})();
|
||
|
||
/* theme toggle + version */
|
||
/*@BUILD:include:src/chrome.js@*/
|
||
|
||
addEventListener("keydown",(e)=>{
|
||
const tag=e.target?e.target.tagName:""; if(tag==="INPUT"||tag==="TEXTAREA"||tag==="SELECT") return;
|
||
if(e.key===" "){ e.preventDefault(); toggle(); }
|
||
else if(e.key==="t"||e.key==="T"){ tapTempo(); }
|
||
else if(e.key==="ArrowUp"){ e.preventDefault(); setBpm(state.bpm+1); }
|
||
else if(e.key==="ArrowDown"){ e.preventDefault(); setBpm(state.bpm-1); }
|
||
else if(e.key==="ArrowRight"){ loadTrack(trackIdx+1); }
|
||
else if(e.key==="ArrowLeft"){ loadTrack(trackIdx-1); }
|
||
});
|
||
|
||
/* ========================= INIT ============================================== */
|
||
{ const ht=tracksFromHash(); if(ht) tracks=ht; }
|
||
loadTrack(0);
|
||
requestAnimationFrame(frame);
|
||
window.currentProgramString = function(){ var t=tracks[trackIdx]||{}; return setupToPatch({bpm:state.bpm, volume:state.volume, lanes:t.lanes||[]}); };
|
||
window.loadProgramString = function(plain){ var s=patchToSetup(plain); tracks=[{name:"Program", ...s}]; trackIdx=0; loadTrack(0); };
|
||
/*@BUILD:include:src/progbox.js@*/
|
||
</script>
|
||
|
||
/*@BUILD:include:src/footer.html@*/
|
||
</body>
|
||
</html>
|