Add visual melody editor for classical pieces
New editor screen lets users fix incorrect melodies without editing code: - Canvas-based timeline with 5 lane rows (Red/Yellow/Blue/Green/Kick) - Click to add notes, click existing notes to select, drag to move - Delete/Backspace to remove selected note - Arrow keys to nudge timing and lane - Beat grid snapping (1/4 beat resolution) - BPM adjustment - Zoom slider for timeline scale - Preview with orchestral background + melody clicks - Save to localStorage (auto-loaded when playing the piece) - Copy JSON to clipboard for sharing corrections - Reset button to restore original melody - Edit button appears in menu only for classical pieces Edits are loaded in buildClassicalNotes() so saved corrections are used during gameplay automatically. https://claude.ai/code/session_01TFVkxq4PYhkSFGYHEi4uqY
This commit is contained in:
+338
-2
@@ -131,6 +131,7 @@ canvas{display:block;}
|
|||||||
|
|
||||||
<div class="sticky-start">
|
<div class="sticky-start">
|
||||||
<button class="smbtn" onclick="startRemap()">🎮 Remap Pads</button>
|
<button class="smbtn" onclick="startRemap()">🎮 Remap Pads</button>
|
||||||
|
<button class="smbtn" id="edit-btn" onclick="openEditor()" style="display:none;">✏️ Edit Melody</button>
|
||||||
<button class="bigbtn" onclick="startGame()">▶ Start</button>
|
<button class="bigbtn" onclick="startGame()">▶ Start</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -179,6 +180,25 @@ canvas{display:block;}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- EDITOR -->
|
||||||
|
<div class="screen" id="sed" style="padding:0;">
|
||||||
|
<div id="ed-toolbar" style="display:flex;align-items:center;gap:8px;padding:6px 12px;background:#13131f;border-bottom:1px solid #333;flex-wrap:wrap;">
|
||||||
|
<button class="smbtn" onclick="closeEditor()">← Back</button>
|
||||||
|
<span id="ed-title" style="font-weight:bold;color:#ccc;margin-right:8px;">Editing: —</span>
|
||||||
|
<label style="color:#aaa;font-size:12px;">BPM: <input type="number" id="ed-bpm" value="120" min="30" max="300" style="width:55px;background:#1a1a2e;color:#fff;border:1px solid #444;padding:2px 4px;border-radius:3px;" onchange="edUpdateBpm()"></label>
|
||||||
|
<label style="color:#aaa;font-size:12px;">Zoom: <input type="range" id="ed-zoom" min="20" max="200" value="80" style="width:80px;" oninput="edDraw()"></label>
|
||||||
|
<button class="smbtn" onclick="edPreview()" id="ed-pv-btn">▶ Preview</button>
|
||||||
|
<button class="smbtn" onclick="edSave()" style="background:#38a169;">💾 Save</button>
|
||||||
|
<button class="smbtn" onclick="edExport()">📋 Copy JSON</button>
|
||||||
|
<button class="smbtn red" onclick="edReset()">↺ Reset</button>
|
||||||
|
<span id="ed-status" style="color:#888;font-size:11px;margin-left:auto;"></span>
|
||||||
|
</div>
|
||||||
|
<div id="ed-help" style="padding:4px 12px;background:#0d0d18;color:#666;font-size:11px;border-bottom:1px solid #222;">
|
||||||
|
Click = add note | Click note = select | Drag = move | Delete/Backspace = remove selected | Scroll = pan timeline
|
||||||
|
</div>
|
||||||
|
<canvas id="ed-canvas" style="display:block;width:100%;cursor:crosshair;"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// ── LANE DEFINITIONS ──────────────────────────────────
|
// ── LANE DEFINITIONS ──────────────────────────────────
|
||||||
const LANES=[
|
const LANES=[
|
||||||
@@ -3086,12 +3106,15 @@ const WORLD_PIECES=(function(){
|
|||||||
var SYNTH_GENRES={"Prime":PRIME_PIECES,"Singalong":SINGALONG_PIECES,"Christmas":CHRISTMAS_PIECES,"Folk":FOLK_PIECES,"Spirituals":SPIRITUAL_PIECES,"World":WORLD_PIECES,"Classical":CLASSICAL_PIECES,"Video Games":VIDEOGAME_PIECES,"Chiptune Originals":CHIPTUNE_PIECES};
|
var SYNTH_GENRES={"Prime":PRIME_PIECES,"Singalong":SINGALONG_PIECES,"Christmas":CHRISTMAS_PIECES,"Folk":FOLK_PIECES,"Spirituals":SPIRITUAL_PIECES,"World":WORLD_PIECES,"Classical":CLASSICAL_PIECES,"Video Games":VIDEOGAME_PIECES,"Chiptune Originals":CHIPTUNE_PIECES};
|
||||||
|
|
||||||
function buildClassicalNotes(piece){
|
function buildClassicalNotes(piece){
|
||||||
return piece.melody.filter(function(m){return activePads[m.l];})
|
// Check for saved melody edits
|
||||||
|
var saved=edLoadSaved(piece.n);
|
||||||
|
var melody=saved?saved.melody:piece.melody;
|
||||||
|
return melody.filter(function(m){return activePads[m.l];})
|
||||||
.map(function(m){
|
.map(function(m){
|
||||||
var freq=261.63;
|
var freq=261.63;
|
||||||
for(var i=0;i<piece.bg.length;i++){
|
for(var i=0;i<piece.bg.length;i++){
|
||||||
var b=piece.bg[i];
|
var b=piece.bg[i];
|
||||||
if(Math.abs(b.t-m.t)<0.01&&b.i!=='bass'&&b.i!=='timpani'){freq=b.f;break;}
|
if(Math.abs(b.t-m.t)<0.05&&b.i!=='bass'&&b.i!=='timpani'){freq=b.f;break;}
|
||||||
}
|
}
|
||||||
return{time:m.t/gameSpeed,lane:m.l,freq:freq,hit:false,missed:false};
|
return{time:m.t/gameSpeed,lane:m.l,freq:freq,hit:false,missed:false};
|
||||||
})
|
})
|
||||||
@@ -3250,6 +3273,7 @@ function buildMenu(){
|
|||||||
pl.appendChild(row);
|
pl.appendChild(row);
|
||||||
});
|
});
|
||||||
updateHitModeVisibility();
|
updateHitModeVisibility();
|
||||||
|
updateEditBtn();
|
||||||
}
|
}
|
||||||
buildMenu();loadSettings();
|
buildMenu();loadSettings();
|
||||||
function addPlayer(){players.push({name:"Player "+(players.length+1)});buildMenu();}
|
function addPlayer(){players.push({name:"Player "+(players.length+1)});buildMenu();}
|
||||||
@@ -3327,6 +3351,318 @@ function renderScores(){
|
|||||||
}
|
}
|
||||||
refreshMiniLB();
|
refreshMiniLB();
|
||||||
|
|
||||||
|
// ── MELODY EDITOR ─────────────────────────────────────
|
||||||
|
var edPiece=null,edMelody=[],edBpm=120,edSelected=-1,edScrollX=0,edDragging=false,edDragNote=-1,edDragOffX=0,edDragOffY=0,edPreviewing=false;
|
||||||
|
var ED_LANE_H=50,ED_KICK_H=40,ED_HEAD=0,ED_NOTE_W=14,ED_NOTE_H=16;
|
||||||
|
var ED_COLORS=["#e53e3e","#d69e2e","#3182ce","#38a169","#805ad5"];
|
||||||
|
var ED_NAMES=["Red","Yellow","Blue","Green","Kick"];
|
||||||
|
|
||||||
|
function getEdCanvas(){return document.getElementById("ed-canvas");}
|
||||||
|
function getEdCtx(){return getEdCanvas().getContext("2d");}
|
||||||
|
function edPxPerSec(){return parseInt(document.getElementById("ed-zoom").value);}
|
||||||
|
function edTimeToX(t){return t*edPxPerSec()-edScrollX;}
|
||||||
|
function edXToTime(x){return(x+edScrollX)/edPxPerSec();}
|
||||||
|
function edYToLane(y){
|
||||||
|
if(y<ED_HEAD)return-1;
|
||||||
|
var laneY=y-ED_HEAD;
|
||||||
|
var padH=ED_LANE_H;
|
||||||
|
for(var i=0;i<4;i++){if(laneY<padH*(i+1))return i;}
|
||||||
|
return 4; // kick
|
||||||
|
}
|
||||||
|
function edLaneToY(l){
|
||||||
|
if(l<4)return ED_HEAD+l*ED_LANE_H+ED_LANE_H/2;
|
||||||
|
return ED_HEAD+4*ED_LANE_H+ED_KICK_H/2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditor(){
|
||||||
|
var pieces=SYNTH_GENRES[curGenre];
|
||||||
|
if(!pieces||!pieces[curIdx])return;
|
||||||
|
edPiece=pieces[curIdx];
|
||||||
|
if(!edPiece.isClassical)return;
|
||||||
|
// Load saved edits or use original
|
||||||
|
var saved=edLoadSaved(edPiece.n);
|
||||||
|
if(saved){
|
||||||
|
edMelody=saved.melody.map(function(m){return{t:m.t,l:m.l};});
|
||||||
|
edBpm=saved.bpm||edPiece.bpm;
|
||||||
|
}else{
|
||||||
|
edMelody=edPiece.melody.map(function(m){return{t:m.t,l:m.l};});
|
||||||
|
edBpm=edPiece.bpm;
|
||||||
|
}
|
||||||
|
edSelected=-1;edScrollX=0;
|
||||||
|
document.getElementById("ed-title").textContent="Editing: "+edPiece.n;
|
||||||
|
document.getElementById("ed-bpm").value=edBpm;
|
||||||
|
show("sed");
|
||||||
|
setTimeout(edResize,50);
|
||||||
|
}
|
||||||
|
function closeEditor(){stopPreview();edPreviewing=false;document.getElementById("ed-pv-btn").textContent="▶ Preview";show("sm");}
|
||||||
|
|
||||||
|
function edResize(){
|
||||||
|
var cv=getEdCanvas();
|
||||||
|
var rect=cv.parentElement.getBoundingClientRect();
|
||||||
|
var toolH=document.getElementById("ed-toolbar").offsetHeight+document.getElementById("ed-help").offsetHeight;
|
||||||
|
cv.width=rect.width;
|
||||||
|
cv.height=window.innerHeight-toolH-cv.getBoundingClientRect().top+window.scrollY;
|
||||||
|
ED_LANE_H=Math.floor((cv.height-ED_KICK_H)/4);
|
||||||
|
edDraw();
|
||||||
|
}
|
||||||
|
window.addEventListener("resize",function(){if(document.getElementById("sed").classList.contains("active"))edResize();});
|
||||||
|
|
||||||
|
function edDraw(){
|
||||||
|
var cv=getEdCanvas(),ctx=getEdCtx(),W=cv.width,H=cv.height;
|
||||||
|
ctx.clearRect(0,0,W,H);
|
||||||
|
var pps=edPxPerSec();
|
||||||
|
|
||||||
|
// Draw lane backgrounds
|
||||||
|
for(var i=0;i<4;i++){
|
||||||
|
ctx.fillStyle=i%2===0?"#0e0e1a":"#111122";
|
||||||
|
ctx.fillRect(0,ED_HEAD+i*ED_LANE_H,W,ED_LANE_H);
|
||||||
|
// Lane label
|
||||||
|
ctx.fillStyle=ED_COLORS[i];ctx.globalAlpha=0.3;
|
||||||
|
ctx.font="bold 12px sans-serif";
|
||||||
|
ctx.fillText(ED_NAMES[i],4,ED_HEAD+i*ED_LANE_H+15);
|
||||||
|
ctx.globalAlpha=1;
|
||||||
|
}
|
||||||
|
// Kick lane
|
||||||
|
ctx.fillStyle="#0a0a14";
|
||||||
|
ctx.fillRect(0,ED_HEAD+4*ED_LANE_H,W,ED_KICK_H);
|
||||||
|
ctx.fillStyle=ED_COLORS[4];ctx.globalAlpha=0.3;
|
||||||
|
ctx.font="bold 12px sans-serif";
|
||||||
|
ctx.fillText("Kick",4,ED_HEAD+4*ED_LANE_H+15);
|
||||||
|
ctx.globalAlpha=1;
|
||||||
|
|
||||||
|
// Grid lines (every beat and every second)
|
||||||
|
var beatSec=60/edBpm;
|
||||||
|
var startBeat=Math.floor(edXToTime(0)/beatSec);
|
||||||
|
var endBeat=Math.ceil(edXToTime(W)/beatSec);
|
||||||
|
for(var b=startBeat;b<=endBeat;b++){
|
||||||
|
var bx=edTimeToX(b*beatSec);
|
||||||
|
if(bx<0||bx>W)continue;
|
||||||
|
ctx.strokeStyle=b%4===0?"#444":"#222";
|
||||||
|
ctx.lineWidth=b%4===0?1.5:0.5;
|
||||||
|
ctx.beginPath();ctx.moveTo(bx,ED_HEAD);ctx.lineTo(bx,H);ctx.stroke();
|
||||||
|
if(b%4===0){
|
||||||
|
ctx.fillStyle="#555";ctx.font="10px sans-serif";
|
||||||
|
ctx.fillText((b*beatSec).toFixed(1)+"s",bx+2,ED_HEAD+H-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Second markers
|
||||||
|
var startSec=Math.floor(edXToTime(0));
|
||||||
|
var endSec=Math.ceil(edXToTime(W));
|
||||||
|
for(var s=startSec;s<=endSec;s++){
|
||||||
|
var sx=edTimeToX(s);
|
||||||
|
if(sx<0||sx>W)continue;
|
||||||
|
ctx.strokeStyle="#333";ctx.lineWidth=0.5;
|
||||||
|
ctx.beginPath();ctx.moveTo(sx,0);ctx.lineTo(sx,ED_HEAD);ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lane separator lines
|
||||||
|
for(i=1;i<5;i++){
|
||||||
|
var ly=ED_HEAD+i*ED_LANE_H;
|
||||||
|
ctx.strokeStyle="#333";ctx.lineWidth=1;
|
||||||
|
ctx.beginPath();ctx.moveTo(0,ly);ctx.lineTo(W,ly);ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw notes
|
||||||
|
edMelody.forEach(function(m,idx){
|
||||||
|
var nx=edTimeToX(m.t)-ED_NOTE_W/2;
|
||||||
|
var ny=edLaneToY(m.l)-ED_NOTE_H/2;
|
||||||
|
if(nx+ED_NOTE_W<0||nx>W)return;
|
||||||
|
ctx.fillStyle=ED_COLORS[m.l]||"#888";
|
||||||
|
ctx.globalAlpha=idx===edSelected?1:0.8;
|
||||||
|
ctx.fillRect(nx,ny,ED_NOTE_W,ED_NOTE_H);
|
||||||
|
if(idx===edSelected){
|
||||||
|
ctx.strokeStyle="#fff";ctx.lineWidth=2;
|
||||||
|
ctx.strokeRect(nx-1,ny-1,ED_NOTE_W+2,ED_NOTE_H+2);
|
||||||
|
}
|
||||||
|
ctx.globalAlpha=1;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Duration marker
|
||||||
|
if(edPiece){
|
||||||
|
var dx=edTimeToX(edPiece.duration);
|
||||||
|
if(dx>0&&dx<W){
|
||||||
|
ctx.strokeStyle="#e53e3e";ctx.lineWidth=2;ctx.setLineDash([4,4]);
|
||||||
|
ctx.beginPath();ctx.moveTo(dx,0);ctx.lineTo(dx,H);ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
ctx.fillStyle="#e53e3e";ctx.font="10px sans-serif";
|
||||||
|
ctx.fillText("END",dx+3,12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status
|
||||||
|
document.getElementById("ed-status").textContent=edMelody.length+" notes | "+
|
||||||
|
(edSelected>=0?"Selected #"+edSelected+" (t="+edMelody[edSelected].t.toFixed(3)+"s, "+ED_NAMES[edMelody[edSelected].l]+")":"Click to add, click note to select");
|
||||||
|
}
|
||||||
|
|
||||||
|
function edHitTest(x,y){
|
||||||
|
for(var i=edMelody.length-1;i>=0;i--){
|
||||||
|
var m=edMelody[i];
|
||||||
|
var nx=edTimeToX(m.t)-ED_NOTE_W/2;
|
||||||
|
var ny=edLaneToY(m.l)-ED_NOTE_H/2;
|
||||||
|
if(x>=nx&&x<=nx+ED_NOTE_W&&y>=ny&&y<=ny+ED_NOTE_H)return i;
|
||||||
|
}
|
||||||
|
return-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouse handlers
|
||||||
|
function edMouseDown(e){
|
||||||
|
var cv=getEdCanvas(),rect=cv.getBoundingClientRect();
|
||||||
|
var mx=e.clientX-rect.left,my=e.clientY-rect.top;
|
||||||
|
var hit=edHitTest(mx,my);
|
||||||
|
if(hit>=0){
|
||||||
|
edSelected=hit;
|
||||||
|
edDragging=true;edDragNote=hit;
|
||||||
|
edDragOffX=mx-edTimeToX(edMelody[hit].t);
|
||||||
|
edDragOffY=my-edLaneToY(edMelody[hit].l);
|
||||||
|
edDraw();
|
||||||
|
}else{
|
||||||
|
// Add new note
|
||||||
|
var t=edXToTime(mx);
|
||||||
|
var l=edYToLane(my);
|
||||||
|
if(l>=0&&l<=4&&t>=0){
|
||||||
|
// Snap to nearest beat subdivision (1/4 beat)
|
||||||
|
var snap=60/edBpm/4;
|
||||||
|
t=Math.round(t/snap)*snap;
|
||||||
|
edMelody.push({t:t,l:l});
|
||||||
|
edMelody.sort(function(a,b){return a.t-b.t;});
|
||||||
|
edSelected=edMelody.findIndex(function(m){return m.t===t&&m.l===l;});
|
||||||
|
edDraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function edMouseMove(e){
|
||||||
|
if(!edDragging||edDragNote<0)return;
|
||||||
|
var cv=getEdCanvas(),rect=cv.getBoundingClientRect();
|
||||||
|
var mx=e.clientX-rect.left,my=e.clientY-rect.top;
|
||||||
|
var t=edXToTime(mx-edDragOffX);
|
||||||
|
var l=edYToLane(my);
|
||||||
|
var snap=60/edBpm/4;
|
||||||
|
t=Math.max(0,Math.round(t/snap)*snap);
|
||||||
|
if(l>=0&&l<=4){
|
||||||
|
edMelody[edDragNote].t=t;
|
||||||
|
edMelody[edDragNote].l=l;
|
||||||
|
edSelected=edDragNote;
|
||||||
|
edDraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function edMouseUp(){
|
||||||
|
if(edDragging){
|
||||||
|
edDragging=false;edDragNote=-1;
|
||||||
|
edMelody.sort(function(a,b){return a.t-b.t;});
|
||||||
|
// Re-find selected after sort
|
||||||
|
edDraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function edWheel(e){
|
||||||
|
e.preventDefault();
|
||||||
|
edScrollX+=e.deltaX||e.deltaY;
|
||||||
|
if(edScrollX<0)edScrollX=0;
|
||||||
|
edDraw();
|
||||||
|
}
|
||||||
|
function edKeyDown(e){
|
||||||
|
if(!document.getElementById("sed").classList.contains("active"))return;
|
||||||
|
if((e.key==="Delete"||e.key==="Backspace")&&edSelected>=0){
|
||||||
|
edMelody.splice(edSelected,1);
|
||||||
|
edSelected=-1;
|
||||||
|
edDraw();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
if(e.key==="ArrowLeft"&&edSelected>=0){
|
||||||
|
var snap=60/edBpm/4;
|
||||||
|
edMelody[edSelected].t=Math.max(0,edMelody[edSelected].t-snap);
|
||||||
|
edMelody.sort(function(a,b){return a.t-b.t;});
|
||||||
|
edDraw();e.preventDefault();
|
||||||
|
}
|
||||||
|
if(e.key==="ArrowRight"&&edSelected>=0){
|
||||||
|
var snap2=60/edBpm/4;
|
||||||
|
edMelody[edSelected].t+=snap2;
|
||||||
|
edMelody.sort(function(a,b){return a.t-b.t;});
|
||||||
|
edDraw();e.preventDefault();
|
||||||
|
}
|
||||||
|
if(e.key==="ArrowUp"&&edSelected>=0){
|
||||||
|
edMelody[edSelected].l=Math.max(0,edMelody[edSelected].l-1);
|
||||||
|
edDraw();e.preventDefault();
|
||||||
|
}
|
||||||
|
if(e.key==="ArrowDown"&&edSelected>=0){
|
||||||
|
edMelody[edSelected].l=Math.min(4,edMelody[edSelected].l+1);
|
||||||
|
edDraw();e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup editor canvas events
|
||||||
|
(function(){
|
||||||
|
var cv=document.getElementById("ed-canvas");
|
||||||
|
cv.addEventListener("mousedown",edMouseDown);
|
||||||
|
cv.addEventListener("mousemove",edMouseMove);
|
||||||
|
cv.addEventListener("mouseup",edMouseUp);
|
||||||
|
cv.addEventListener("mouseleave",edMouseUp);
|
||||||
|
cv.addEventListener("wheel",edWheel,{passive:false});
|
||||||
|
document.addEventListener("keydown",edKeyDown);
|
||||||
|
})();
|
||||||
|
|
||||||
|
function edUpdateBpm(){edBpm=parseInt(document.getElementById("ed-bpm").value)||120;edDraw();}
|
||||||
|
|
||||||
|
function edPreview(){
|
||||||
|
if(edPreviewing){stopPreview();edPreviewing=false;document.getElementById("ed-pv-btn").textContent="▶ Preview";return;}
|
||||||
|
// Play the piece with current edits
|
||||||
|
var ac=getAc(true);ac.resume();
|
||||||
|
var piece=edPiece;
|
||||||
|
for(var i=0;i<piece.bg.length;i++){
|
||||||
|
var n=piece.bg[i];
|
||||||
|
playSynth(ac,n.f,ac.currentTime+0.1+n.t,n.d,n.i,n.v*1.2,previewNodes);
|
||||||
|
}
|
||||||
|
// Also play a click/drum sound on each melody note so user hears the hits
|
||||||
|
edMelody.forEach(function(m){
|
||||||
|
var freq=[329,392,440,349,220][m.l]||329;
|
||||||
|
playSynth(ac,freq,ac.currentTime+0.1+m.t,0.1,'woodwind',0.5,previewNodes);
|
||||||
|
});
|
||||||
|
edPreviewing=true;
|
||||||
|
document.getElementById("ed-pv-btn").textContent="⏹ Stop";
|
||||||
|
// Auto-stop after duration
|
||||||
|
var dur=piece.duration||90;
|
||||||
|
setTimeout(function(){if(edPreviewing){edPreviewing=false;document.getElementById("ed-pv-btn").textContent="▶ Preview";}},dur*1000+500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save/load edits to localStorage
|
||||||
|
var ED_STORAGE_KEY="drgame_edits";
|
||||||
|
function edGetAllSaved(){try{return JSON.parse(localStorage.getItem(ED_STORAGE_KEY))||{};}catch(e){return{};}}
|
||||||
|
function edLoadSaved(name){var all=edGetAllSaved();return all[name]||null;}
|
||||||
|
function edSave(){
|
||||||
|
var all=edGetAllSaved();
|
||||||
|
all[edPiece.n]={melody:edMelody.map(function(m){return{t:m.t,l:m.l};}),bpm:edBpm};
|
||||||
|
try{localStorage.setItem(ED_STORAGE_KEY,JSON.stringify(all));}catch(e){}
|
||||||
|
document.getElementById("ed-status").textContent="Saved! ("+edMelody.length+" notes)";
|
||||||
|
}
|
||||||
|
function edReset(){
|
||||||
|
if(!confirm("Reset to original melody? This discards your edits."))return;
|
||||||
|
var all=edGetAllSaved();delete all[edPiece.n];
|
||||||
|
try{localStorage.setItem(ED_STORAGE_KEY,JSON.stringify(all));}catch(e){}
|
||||||
|
edMelody=edPiece.melody.map(function(m){return{t:m.t,l:m.l};});
|
||||||
|
edBpm=edPiece.bpm;
|
||||||
|
document.getElementById("ed-bpm").value=edBpm;
|
||||||
|
edSelected=-1;edDraw();
|
||||||
|
}
|
||||||
|
function edExport(){
|
||||||
|
var data=JSON.stringify({name:edPiece.n,bpm:edBpm,melody:edMelody},null,2);
|
||||||
|
navigator.clipboard.writeText(data).then(function(){
|
||||||
|
document.getElementById("ed-status").textContent="Copied to clipboard!";
|
||||||
|
}).catch(function(){
|
||||||
|
// Fallback: show in prompt
|
||||||
|
prompt("Copy this JSON:",data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide edit button based on whether current genre has classical pieces
|
||||||
|
function updateEditBtn(){
|
||||||
|
var pieces=SYNTH_GENRES[curGenre];
|
||||||
|
var btn=document.getElementById("edit-btn");
|
||||||
|
if(pieces&&pieces[curIdx]&&pieces[curIdx].isClassical){
|
||||||
|
btn.style.display="";
|
||||||
|
}else{
|
||||||
|
btn.style.display="none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── GAME START ─────────────────────────────────────────
|
// ── GAME START ─────────────────────────────────────────
|
||||||
function startGame(){
|
function startGame(){
|
||||||
stopPreview();
|
stopPreview();
|
||||||
|
|||||||
Reference in New Issue
Block a user